$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

A Tour of Goをやって演習問題を解いてみた

Goをやることになったので、とりあえずA Tour of Goをやって、演習問題を解いてみた。直近ではgoroutineは使わなそうだったので、ひとまずそれ以外の演習問題をした。

回答は https://github.com/shibayu36/golang-playground/tree/master/go-tour に置いている。

Exercise: Loops and Functions

for文の使い方を学べる。for文の初期化ステートメントと条件式を使えば解ける。

package main

import (
	"fmt"
	"math"
)

func Sqrt(x float64) float64 {
	z := 1.0
	for diff := 1.0; math.Abs(diff) > 1e-10; {
		diff = ((z * z) - x) / (2 * z)
		z = z - diff
	}
	return z
}

func main() {
	fmt.Println(Sqrt(3))
	fmt.Println(math.Sqrt(3))
}

Exercise: Slices

  • rangeをうまく使うと綺麗にかけた
  • 多次元配列だと、ループでmakeしなければならない
  • uint8()でキャストしないといけない
package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
	pic := make([][]uint8, dy)

	for y := range pic {
		pic[y] = make([]uint8, dx)
		for x := range pic[y] {
			pic[y][x] = uint8((x + y) / 2)
		}
	}

	return pic
}

func main() {
	pic.Show(Pic)
}

Exercise: Maps

  • string.Fieldsを使ってワード分割
  • mapの値を増やしていくだけで良い
package main

import (
	"strings"
	"golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
	words := strings.Fields(s)
	wordToCount := make(map[string]int)
	for _, word := range words {
		wordToCount[word]++
	}
	return wordToCount
}

func main() {
	wc.Test(WordCount)
}

Exercise: Fibonacci closure

クロージャで変数を二つ持っておくと綺麗に書ける。

package main

import "fmt"

func fibonacci() func() int {
	a1, a2 := 0, 1
	return func() int {
		ret := a1
		a1, a2 = a2, a1 + a2
		return ret
	}
}

func main() {
	f := fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}

Exercise: Stringers

typeにメソッドを生やす練習

package main

import "fmt"

type IPAddr [4]byte

func (ipAddr IPAddr) String() string {
	return fmt.Sprintf("%d.%d.%d.%d", ipAddr[0], ipAddr[1], ipAddr[2], ipAddr[3])
}

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}
	for name, ip := range hosts {
		fmt.Printf("%v: %v\n", name, ip)
	}
}

Exercise: Errors

Errorメソッドを生やすだけで簡単に独自エラーを定義できる。

package main

import (
	"fmt"
	"math"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string {
	return fmt.Sprintf("cannot Sqrt negative number: %f", e)
}

func Sqrt(x float64) (float64, error) {
	if x < 0 {
		return 0, ErrNegativeSqrt(x)
	}

	z := 1.0
	for diff := 1.0; math.Abs(diff) > 1e-10; {
		diff = ((z * z) - x) / (2 * z)
		z = z - diff
	}
	return z, nil
}

func main() {
	fmt.Println(Sqrt(2))
	fmt.Println(Sqrt(-2))
}

Exercise: Readers

インターフェース定義に合わせて実装する練習。

package main

import "golang.org/x/tour/reader"

type MyReader struct{}

func (r MyReader) Read(buf []byte) (int, error) {
	for i := range buf {
		buf[i] = 'A'
	}
	return len(buf), nil
}

func main() {
	reader.Validate(MyReader{})
}

Exercise: rot13Reader

非公開メソッドで変換を書きつつ、Readを実装。

package main

import (
	"io"
	"os"
	"strings"
)

type rot13Reader struct {
	r io.Reader
}

func (reader *rot13Reader) Read(buf []byte) (int, error) {
	n, err := reader.r.Read(buf)
	if err != nil {
		return 0, err
	}

	for i := 0; i < n; i++ {
		buf[i] = reader.rot13(buf[i])
	}
	return n, nil
}

func (reader *rot13Reader) rot13(b byte) (byte) {
	if ('a' <= b && b <= 'm') || ('A' <= b && b <= 'M') {
		return b + 13
	}
	if ('n' <= b && b <= 'z') || ('N' <= b && b <= 'Z') {
		return b - 13
	}
	return b
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

Exercise: Images

自分でImage typeを定義しつつ、インターフェース仕様を満たすように実装。

package main

import (
	"image"
	"image/color"

	"golang.org/x/tour/pic"
)

type Image struct {
	width  int
	height int
}

func (i Image) ColorModel() color.Model {
	return color.RGBAModel
}

func (i Image) Bounds() image.Rectangle {
	return image.Rect(0, 0, i.width, i.height)
}

func (i Image) At(x, y int) color.Color {
	v := uint8((x + y) / 2)
	return color.RGBA{v, v, 255, 255}
}

func main() {
	m := Image{256, 256}
	pic.ShowImage(m)
}

まとめ

今回はA Tour of Goをして演習問題を解いてみたので、その回答を載せてみた。続いてみんなのGo言語を読んでいきたいと思う。

golangの編集環境を再整備する

最近golangをやり始めたので、編集環境を再整備した。

基本設定

昔自分で書いた以下の二つを再度やったら、これだけでかなり快適な環境になった...昔の自分のブログ便利...残しておくものだな...

blog.shibayu36.org
blog.shibayu36.org

ハードタブの表示の設定

あと、Emacsでwhitespace-modeを利用してハードタブなどを可視化するようにしているのだけど、これがgoだとうざく見えてしまうので、上の設定に引き続き、その部分だけ調整した。


以下がwhitespace-modeの設定。whitespace-modeの設定 - syohex’s diaryEmacs の whitespace-mode でタブと行末スペースと全角スペースに色付けするメモ - 牌語備忘録 -pygoあたりを参考にした。

(require 'whitespace)
(setq whitespace-style
      '(face       ;; faceで可視化
        trailing   ;; 行末
        tabs       ;; タブ
        spaces     ;; スペース
        space-mark ;; 表示のマッピング
        tab-mark))
(setq whitespace-display-mappings
      '((space-mark ?\u3000 [?\u25a1])
        ;; WARNING: the mapping below has a problem.
        ;; When a TAB occupies exactly one column, it will display the
        ;; character ?\xBB at that column followed by a TAB which goes to
        ;; the next TAB column.
        ;; If this is a problem for you, please, comment the line below.
        (tab-mark ?\t [?\u00BB ?\t] [?\\ ?\t])))

;; スペースは全角のみを可視化
(setq whitespace-space-regexp "\\(\u3000+\\)")
(set-face-attribute 'whitespace-trailing nil
                    :foreground "DeepPink"
                    :background nil
                    :underline t)
(set-face-attribute 'whitespace-tab nil
                    :foreground "LightSkyBlue"
                    :background nil
                    :underline t)
(set-face-attribute 'whitespace-space nil
                    :foreground "GreenYellow"
                    :background nil
                    :weight 'bold)

(global-whitespace-mode 1)


で、あとはこのwhitespace-styleのtabsの設定が、go-modeのときだけなくなるようにすればよい。次のように書いたら良い。

(defun shibayu36/go-mode-hook ()
  ;; golangではハードタブを可視化しない
  (setq whitespace-style
      '(face
        trailing
        spaces
        space-mark)))
(add-hook 'go-mode-hook 'shibayu36/go-mode-hook)

まとめ

今回はEmacsgolangの編集環境の再整備をした。とはいっても昔の設定をそのまま使うだけで非常に良い編集環境ができあがるので、golangはすごい。

ScalaでHikariCPを使ってDBのコネクションプールを利用する

ScalaでのJDBCによるDB操作の勉強をした - $shibayu36->blog; の続き。今回はHikariCP を利用して、DBのコネクションプールをScalaで利用してみたのでメモ。DBにはPostgreSQLを利用した。

依存の追加

build.sbtに以下を追加。

libraryDependencies += "org.postgresql" % "postgresql" % "42.1.4"
libraryDependencies += "com.zaxxer" % "HikariCP" % "2.7.1"

単純にDB接続してみる

https://github.com/brettwooldridge/HikariCP#initializationhttps://jyn.jp/java-hikaricp-mysql-sqlite/ あたりを参考にした。

とりあえず接続してINSERTしたりSELECTしたりする例はこんな感じ。

import com.zaxxer.hikari.{ HikariConfig, HikariDataSource }
import scala.util.Random

/**
 $ createdb hikaricp-example
 $ psql hikaricp-example
 CREATE TABLE person (
   id SERIAL NOT NULL PRIMARY KEY,
   name VARCHAR(254) NOT NULL
 );
 */
object HikariCPExample {
  def main(args: scala.Array[String]) = {
    val config = new HikariConfig()
    config.setDriverClassName("org.postgresql.Driver")
    config.setJdbcUrl("jdbc:postgresql:hikaricp-example")
    config.setUsername("dbuser")
    config.setPassword("dbuser")
    val ds = new HikariDataSource(config)

    val conn = ds.getConnection()

    val newName = Random.alphanumeric.take(10).mkString
    println(newName)
    val st1 = conn.prepareStatement("INSERT INTO person (name) VALUES(?)")
    st1.setString(1, newName)
    val rowsInserted = st1.executeUpdate()
    println(rowsInserted + " inserted. name = " + newName)
    st1.close()

    val limit = 3
    val st2 = conn.prepareStatement("SELECT id, name FROM person ORDER BY id DESC LIMIT ?")
    st2.setInt(1, limit)
    val rs2 = st2.executeQuery()
    while (rs2.next()) {
      println("id: " + rs2.getInt(1))
      println("name: " + rs2.getString("name"))
    }
    rs2.close()
    st2.close()

    conn.close()
  }
}
  • HikariConfigで接続などの設定をする
  • HikariDataSourceのインスタンスを作る
  • あとはHikariDataSourceからコネクションを獲得し、利用して、最後にcloseして終了

という感じで利用できる。

並列にコネクションを取得してみる

並列処理でHikariCPを使ってみて、コネクションプールが使われる様子を見てみる。例えば、最大でコネクションプールは3つだけ作るという設定で並列処理してみる。

import com.zaxxer.hikari.{ HikariConfig, HikariDataSource }

object ParallelHikariCPExample {
  def main(args: scala.Array[String]) = {
    val config = new HikariConfig()
    config.setDriverClassName("org.postgresql.Driver")
    config.setJdbcUrl("jdbc:postgresql:hikaricp-example")
    config.setUsername("dbuser")
    config.setPassword("dbuser")
    config.setMaximumPoolSize(3)
    val ds = new HikariDataSource(config)

    (1 to 10).par.foreach { index =>
      val conn = ds.getConnection()
      println(s"Connection $index get connection")
      Thread.sleep(index * 1000)
      conn.close()
      println(s"Connection $index close connection")
    }
  }
}

これを実行してみる。Scalaのスレッド数の上限で処理がブロックしないように適当に設定しておく。

$ sbt -Dscala.concurrent.context.numThreads=16 -Dscala.concurrent.context.maxThreads=16
> runMain ParallelHikariCPExample
[info] Running ParallelHikariCPExample
Connection 8 get connection
Connection 1 get connection
Connection 7 get connection
Connection 1 close connection
Connection 6 get connection
Connection 7 close connection
Connection 5 get connection
Connection 6 close connection
Connection 9 get connection
Connection 8 close connection
Connection 2 get connection
Connection 2 close connection
Connection 3 get connection
Connection 5 close connection
Connection 4 get connection
Connection 3 close connection
Connection 10 get connection
Connection 4 close connection
Connection 9 close connection
Connection 10 close connection

少しわかりづらいが、

  • 最初に三つコネクションを取得でき、そこで上限なので他はgetConnectionでブロックする
  • その後、一つのコネクションがcloseで開放されるたびに、どれかのスレッドがコネクションを獲得できるようになる

という動きをした。コネクションプールを設定した数だけ作り、接続できたことが分かる。

HikariCPのベンチマークを取ってみる

実際にJDBCを直接使っての接続と、HikariCPでコネクションプールを使っての接続で、どの程度速くなるのかベンチマークを取ってみた。ベンチマーク取得にはnanobench を利用する。使い方は以前 nanobenchを使ってJavaのベンチマークを取る - $shibayu36->blog; の記事で書いた。

以下のようにベンチマークコードを書く。

import com.zaxxer.hikari.{ HikariConfig, HikariDataSource }
import java.sql._
import me.geso.nanobench.Benchmark;

object HikariCPBenchmark {
  def main(args: scala.Array[String]): Unit = {
    new Benchmark(HikariCPBenchmarkInner).warmup(1).runByTime(3).timethese().cmpthese()
  }
}

object HikariCPBenchmarkInner {
  @Benchmark.Bench
  def jdbc: Unit = {
    Class.forName("org.postgresql.Driver")

    for (i <- 1 to 100) {
      val conn = DriverManager.getConnection(
        "jdbc:postgresql:hikaricp-example",
        "dbuser",
        "dbuser"
      )
      conn.close()
    }
  }

  @Benchmark.Bench
  def hikaricp: Unit = {
    val config = new HikariConfig()
    config.setDriverClassName("org.postgresql.Driver")
    config.setJdbcUrl("jdbc:postgresql:hikaricp-example")
    config.setUsername("dbuser")
    config.setPassword("dbuser")
    val ds = new HikariDataSource(config)

    for (i <- 1 to 100) {
      val conn = ds.getConnection()
      conn.close()
    }
    ds.close()
  }
}

実行してみる。

> runMain HikariCPBenchmark
[info] Compiling 1 Scala source to /Users/shibayu36/development/src/github.com/shibayu36/scala-playground/target/scala-2.11/classes...
[info] Running HikariCPBenchmark
Warm up: 1

Score:

jdbc: 17 wallclock secs ( 1.16 usr +  2.43 sys =  3.59 CPU) @  9.75/s (n=35)
hikaricp: 10 wallclock secs ( 1.22 usr +  1.89 sys =  3.10 CPU) @ 511.82/s (n=1589)

Comparison chart:

              Rate   jdbc  hikaricp
      jdbc  9.75/s     --      -98%
  hikaricp   512/s  5150%        --
[success] Total time: 43 s, completed Sep 18, 2017 8:16:40 AM

これを見ると、HikariCPを使うと接続が50倍ほど速くなることが分かった。コネクションプール便利。

まとめ

今回はScalaでHikariCPを使ってコネクションプールを利用するのを試してみた。並列でコネクションプールを使うとどのようになるか、実際にどの程度速くなるかなどを試せて勉強になった。