【AWS】DynamoDB local でローカル上にAWS DynamoDB 環境を作る
DynamoDB local
AWS は、ローカル環境上で DynamoDB を使用したアプリケーションを開発・テストするための DynamoDB local
を提供している。
DynamoDB local
は以下の手順でデプロイ可能。
- DynamoDB local をローカルにインストールする
- Apache Maven リポジトリを追加して DynamoDB をデプロイする
- DynamoDB Docker イメージをインストールする
今回は Docker イメージをインストールして検証した。
Usage
Dynamo DB local
docker-compose
でコンテナをデプロイする。
docker-compose.yml
version: '3.7' services: dynamodb-local: image: amazon/dynamodb-local:latest container_name: dynamodb-local ports: - '8000:8000'
$ ls docker-compose.yml $ docker-compose up -d
これで Dynamo DB local
コンテナデプロイは完了。
ホスト OS にポート 8000
を開けているので、 http://localhost:8000/shell
をブラウザで開くと、ローカルの DynamoDB に対してコード経由で処理を実行できるエディタが開ける。
エディタは JS に対応している。
Code(JS)
AWS.config.endpoint = new AWS.Endpoint('http://localhost:8000'); let dynanodb = new AWS.DynamoDB(); let region = 'us-west-2'; AWS.config.update({ region: region, }); let tableName = 'Articles'; let log = (err, data) => { if (err) { console.log(err); } else { console.log(data); } }; // create table let createTable = () => { let tableParams = { TableName: tableName, KeySchema: [{ AttributeName: 'ID', KeyType: 'HASH' }], AttributeDefinitions: [{ AttributeName: 'ID', AttributeType: 'N' }], ProvisionedThroughput: { ReadCapacityUnits: 10, WriteCapacityUnits: 10, }, }; dynamodb.createTable(tableParams, log); }; // check table existence let checkTableParams = { TableName: tableName, }; dynamodb.describeTable(checkTableParams, function (err, data) { if (err) { createTable(); } }); // PUT Item let putParams = { TableName: tableName, Item: { ID: { N: '1' }, Title: { S: 'title' }, Content: { S: 'content' }, }, }; dynamodb.putItem(putParams, log); // GET Item let getParams = { TableName: tableName, Key: { ID: { N: '1' }, }, }; dynamodb.getItem(getParams, log);
=> {} {"Item":{"Title":{"S":"title"},"Content":{"S":"content"},"ID":{"N":"1"}}}
参考文献
【Golang】GoのSQL driver mock `go-sqlmock` を試す
go-sqlmock
とは
go-sqlmock
は Go の database/sql/driver
の実装で、DB ドライバの振る舞いをモック化できるライブラリ。
go-sqlmock
を使用することで、DB ドライバを必要するロジックと、実際の DB ドライバ以降の処理を分離してテストできる。
go-sqlmock
の特徴
DB ドライバに対するクエリ単位でモック設定ができる
sql.DB::Query
や sql.DB::Exec
に指定する個々のクエリの内容ごとに返戻する行や結果を指定できる。
複数パターンのクエリに対して同一の結果を返したい場合も、正規表現で指定可能。
DB ドライバに対する関数呼び出しの順序検証ができる
ドライバのモック化だけでなく、ドライバモックに対して呼ばれたクエリやトランザクション処理の順序を検証する機能も持つ。
その他特徴
- 安定版である
- 並行性と同時接続サポート
- Go1.8 以降の Context に対応:SQL パラメータの命名とモッキング
- 仕様元ソースコードの変更必要なし
- どの
sql/driver
メソッドの振る舞いもモック化できる - 厳密な期待順序マッチングを搭載
- サードパーティライブラリへの依存なし
Usage
Install
$ go get github.com/DATA-DOG/go-sqlmock
検証のため、今回は MySQL ドライバを使用。
$ go get github.com/go-sql-driver/mysql
検証用コード
今回使用する検証用コードは以下に置いてあります。
rennnosuke/go-playground/go-sqlmock
取得クエリ(SELECT)の検証
テスト対象コード
DB ドライバを使用するコードを用意する。
import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) type Article struct { ID int Title string Content string } func GetByID(id int, db *sql.DB) (*Article, error) { row := db.QueryRow("SELECT * FROM ARTICLES WHERE ID = ? AND IS_DELETED = 0", id) e := Article{} if err := row.Scan(&e.ID, &e.Title, &e.Content); err != nil { return nil, fmt.Errorf("failed to scan row: %s", err) } return &Article{ID: e.ID, Title: e.Title, Content: e.Content}, nil }
テストコード
ドライバを使用する実装に対する、go-sqlmock
を使用したテストコードを用意。
import ( "fmt" "testing" "github.com/DATA-DOG/go-sqlmock" ) func TestSQLMock_Select(t *testing.T) { // モックDBの初期化 db, mock, err := sqlmock.New() if err != nil { t.Fatalf("failed to init db mock") } defer db.Close() id := 1 // dbドライバに対する操作のモック定義 columns := []string{"id", "title", "content"} mock.ExpectQuery("SELECT (.+) FROM ARTICLES"). // expectedSQL: 想定される実行クエリをregexpで指定(指定文字列が含まれるかどうかを見る) WithArgs(id). // 想定されるプリペアドステートメントへの引数 WillReturnRows(sqlmock.NewRows(columns).AddRow(1, "test title", "test content")) // 返戻する行情報の指定 // テスト対象関数call article, err := GetByID(id, db) if err != nil { t.Fatalf("failed to get article: %s", err) } fmt.Printf("%v", article) // mock定義の期待操作が順序道理に実行されたか検査 if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("failed to ExpectationWerMet(): %s", err) } }
モック DB の初期化
sqlmock.New()
ファクトリ関数で sql.DB
値への参照と、モックを定義するための SqlMock
型の値を取得できる。
sql.DB
は DB ドライバを使用するロジックに引き渡し、DB ドライバ以降の処理のモックとして使用される。
SqlMock
は主にテスト関数内で、想定される DB ドライバに対する操作と、それらに対する結果を定義する。
// モックDBの初期化
db, mock, err := sqlmock.New()
ドライバへの想定操作と結果の定義
次に、DB ドライバに対してどのようなクエリが実行されるのかを SqlMock::ExpectQuery()
関数で定義する。
この定義は後述する SqlMock::ExpectationsWereMet()
の検証で使用される。
指定するクエリ文字列は regexp
パッケージ準拠の正規表現で記述できる。またデフォルト設定の場合、指定した文字列が実際に呼び出されたクエリに部分マッチすれば検証は Pass される。
クエリにプリペアドステートメントが指定されている場合、WithArts
で実際に挿入する値を指定できる。
また、各々の操作に対してどのような結果を返すのかを SqlMock::WillReturnRows()
で定義している。
返す行のカラムを NewRows()
で、行データを *Rows.AddRow()
で指定している。
// dbドライバに対する操作のモック定義 columns := []string{"id", "title", "content"} mock.ExpectQuery("SELECT (.+) FROM ARTICLES"). // expectedSQL: 想定される実行クエリをregexpで指定(指定文字列が含まれるかどうかを見る) WithArgs(id). // 想定されるプリペアドステートメントへの引数 WillReturnRows(sqlmock.NewRows(columns).AddRow(1, "test title", "test content")) // 返戻する行情報の指定
実際のクエリ呼び出し検証
ExpectationsWereMet checks whether all queued expectations were met in order. If any of them was not met - an error is returned.
SqlMock::ExpectationsWereMet()
は、呼び出した SqlMock::ExpectXXX
に即する操作が、実際に DB ドライバ( sqlmock.New()
から返戻された sql.DB
)に順番どおりに実行されたかどうかを検証する。
// mock定義の期待操作が順序道理に実行されたか検査 if err := mock.ExpectationsWereMet(); err != nil { t.Fatalf("failed to ExpectationWerMet(): %s", err) }
永続化処理(INSERT)の検証
テスト対象コード
func Create(id int, title, content string, db *sql.DB) error { tx, err := db.Begin() defer func() { switch err { case nil: tx.Commit() default: tx.Rollback() } }() if err != nil { return err } _, err = tx.Exec("INSERT INTO ARTICLES (ID, TITLE, CONTENT) VALUES (?, ?, ?)", id, title, content) if err != nil { return fmt.Errorf("failed to insert article: %s", err) } return nil }
テストコード
func TestSQLMock_Insert(t *testing.T) { // モックDBの初期化 // ... id := 1 title := "test title" content := "test content" // dbドライバに対する操作のモック定義 mock.ExpectBegin() mock.ExpectExec("INSERT INTO ARTICLES"). // 想定される実行SQLをregexpで指定(指定文字列が含まれるかどうかを見る) WithArgs(id, title, content). // 想定されるプリペアドステートメントへの引数 WillReturnResult(sqlmock.NewResult(1, 1)) // 想定されるExec関数の結果を指定 mock.ExpectCommit() // テスト対象関数call // ... // mock定義の期待操作が順序道理に実行されたか検証 // ... }
ドライバへの想定操作と結果の定義
SELECT
検証時と異なる点は 2 つ。
トランザクション呼び出しの定義 永続化処理などでトランザクション開始終了処理
Begin()
Commit()
RollBack()
を呼ぶとき、それに合わせてSqlMock.ExpectBegin()
SqlMock.ExpectCommit()
SqlMock.ExpectRollback()
を呼ぶ。Exec 関数呼び出しの定義 永続化処理では
sql.DB::Query()
ではなくsql.DB::Exec()
を使用しているため、SqlMock.ExpectExec()
を使用している。また返戻値が行ではなく更新結果となるため、これをWillReturnResult()
で指定し、返戻値の内容をNewResult()
で定義している。
// dbドライバに対する操作のモック定義 mock.ExpectBegin() mock.ExpectExec("INSERT INTO ARTICLES"). // 想定される実行SQLをregexpで指定(指定文字列が含まれるかどうかを見る) WithArgs(id, title, content). // 想定されるプリペアドステートメントへの引数 WillReturnResult(sqlmock.NewResult(1, 1)) // 想定されるExec関数の結果を指定 mock.ExpectCommit()
UPDATE
処理を実装した場合も、基本検証は INSERT
とほぼ同じ流れになる。
クエリマッチングルールの変更
SqlMock::ExpectQuery()
SqlMock::ExpectExec()
でクエリに含まれるべき文字列を指定する事ができたが、このルールは sqlmock.New()
に sqlmock.QueryMatcherOption()
返戻値を指定することで変更可能になる。
db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
sqlmock.QueryMatcherOption()
には以下の QueryMatcher が指定できる。
sqlmock.QueryMatcherRegexp
: 想定される SQL 文字列を正規表現として使用し、実行クエリの文字列と照合するsqlmock.QueryMatcherEqual
: 想定される SQL 文字列と実行クエリを大文字・小文字含め完全一致で照合する
この QueryMatcher は QueryMatcherFunc
型で、この型にキャスト可能な関数を定義すれば独自の QueryMatcher を定義できる。
type QueryMatcherFunc func(expectedSQL, actualSQL string) error
所感
- テストケースごとに個別にクエリ結果を設定できるのは柔軟でよい
sql.DB
関数呼び出し順検証、DB 周りの処理が頻繁に変更しない前提ならば便利かも- 逐一想定クエリを記述するのはちょっと面倒
- RDB のテーブルスキーマやリレーションは再現できないので、実動環境と乖離しない定義を書くことが求められる1
参考文献
【Golang】GoでTCPソケット通信を実装する
ソケット通信
ソケット通信とは、プログラムから見たときのネットワーク通信を抽象化する概念をさす。通信の終端をソケットと呼ばれるオブジェクトとみなし、ソケットに対して何かしらの入力を行うと、もう一方のソケットから出力される。
このモデルにおいて、ネットワーク階層モデル(OSI,TCP/IP など)上のいわゆるネットワークレイヤ以下の詳細は隠蔽される。例えばパケット(セグメント)がどのように分割され、どのノードを経由して到達するのかなどはプログラムで制御しなくとも良い。
Go のソケット通信
Go では net
パッケージがソケット通信の機能を提供している。受け手側で net.Listen()
、送る側で net.Dial()
関数を呼び出し、それぞれから取得できる Conn
を介してメッセージを送受信できる。
net - The Go Programming Language
Usage
Server
サーバー側はソケットをオープンしたあと、クライアントからの入力を待ち続ける。 今回は TCP 通信で検証し、サーバー・クライアント共々同一ネットワークノード上にあるものとする。
package main import ( "fmt" "io" "net" "os" ) type HandleConnection func(c net.Conn) error func main() { handleConn := func(c net.Conn) error { buf := make([]byte, 1024) for { n, err := c.Read(buf) if err != nil { return err } if n == 0 { break } s := string(buf[:n]) fmt.Println(s) fmt.Fprintf(c, "accept:%s\n", s) } return nil } if err := start(handleConn); err != nil { fmt.Fprintln(os.Stderr, err) } } func start(f HandleConnection) error { ln, err := net.Listen("tcp", "localhost:8080") if err != nil { return err } defer ln.Close() conn, err := ln.Accept() if err != nil { return err } defer conn.Close() for { if err := f(conn); err != nil && err != io.EOF { return err } } }
Conn
(ソケット)が io.Reader
io.Writer
インタフェース要件を満たすので、標準入出力のように扱える。もともと BSD UNIX がソケット通信をファイル読み書きのように扱いたかったという思想があり、この概念を継承している様子。
Client
クライアントは一度きりの入力とした。もちろん繋ぎっぱなしのまま連続して入力もできる。
package main import ( "bufio" "fmt" "net" ) func main() { fmt.Println("client start.") err := start() if err != nil { fmt.Errorf("%s", err) } fmt.Println("client end.") } func start() error { conn, err := net.Dial("tcp", "localhost:8080") if err != nil { return err } defer conn.Close() fmt.Fprintf(conn, "Hello, Socket Connection !") status, err := bufio.NewReader(conn).ReadString('\n') if err != nil { return err } fmt.Println(status) return nil }
実行結果
Server
Hello, Socker Connection !
Client
client start. accept:Hello, Socket Connection ! client end.
net パッケージの実装
ソケット通信の実装部分は OS が使用する物がすでにあり、Go はそれを使用していると思われる。
net
パッケージの MacOS 用のロジックを追っていくと、最終的にシステムコールよりソケット通信 API を実行している様子。
(rawSyscall でシステムコールを実行できる)
syscall/zsyscall_darwin_amd64.go
//go:linkname libc_connect libc_connect //go:cgo_import_dynamic libc_connect connect "/usr/lib/libSystem.B.dylib" // THIS FILE IS GENERATED BY THE COMMAND AT THE TOP; DO NOT EDIT func socket(domain int, typ int, proto int) (fd int, err error) { r0, _, e1 := rawSyscall(funcPC(libc_socket_trampoline), uintptr(domain), uintptr(typ), uintptr(proto)) fd = int(r0) if e1 != 0 { err = errnoErr(e1) } return }
考えられるユースケース
アプリケーションレイヤのペイロードを自由に記述できるので、例えば独自のアプリケーションプロトコルを定義したり、http 通信ほど情報を詰め込みたくないが、何らかの情報を別ノードに送信したい場合などに利用できそう。
参考文献
【GraphQL】gqlgenでGraphQL serverをGoで構築する
gqlgen
gqlgen
は Go の GraphQL ライブラリで、GraphQL をインタフェースとして持つ API サーバを Go で構築できる。
gqlgen
はコード上に GraphQL スキーマをガシガシ書いていくライブラリとは異なり、
GraphQL ファイルに記述するスキーマ情報からコードを自動生成する。
なのでコードがごちゃごちゃしにくく、Go を知らない人でも GraphQL スキーマを書ける人なら誰でも定義を編集できるメリットがある(と個人的に考えている)。
デメリットとしては、自動生成されるコードに関してはブラックボックスになってしまうということと、
読み解いて編集したとてスキーマ更新時に再編集しなければいけないところだろうか。
Usage
プロジェクトの作成
公式のチュートリアルがあり一回通してみたが、これとは別に自分なりにプロジェクトを再構築した。
$ mkdir tmp-gqlgen; cd tmp-gqlgen $ go mod init (Gov1.11以上であれば)
まずはパッケージを入手。
$ go get github.com/99designs/gqlgen
次にプロジェクトルートに GraphQL スキーマファイルを用意。
schema.graphql
type Query { user(id: ID): User! pet(id: ID): Pet! } type User { id: ID name: String } type Pet { id: ID name: String }
次に gqlgen.yml
を用意する。
このファイルは gqlgen
でコードを自動生成する際に必要になる。
# GraphQLスキーマファイルの場所 schema: - ./*.graphql # スキーマGo実装ファイルの生成場所 exec: filename: graph/generated/generated.go package: generated # モデル構造体ファイルの生成場所 model: filename: graph/model/models_gen.go package: model # resolver(GraphQL版controller的なもの)ファイルの生成場所 resolver: layout: follow-schema dir: graph/resolver package: resolver # 不足したスキーマ構造体を自動生成する場所 autobind: - 'github.com/rennnosuke/tmp-gqlgen/graph/model'
gqlgen.yml
が用意できたら、コードの自動生成を実行する。
$ go run github.com/99designs/gqlgen
すると yml で指定したものを含めいくつかコードが生成される。
ファイル名が気に入らなければ、yml 上で変更できる。
tmp-gqlgen ├── go.mod ├── go.sum ├── gqlgen.yml ├── graph │ ├── generated │ │ └── generated.go │ ├── model │ │ └── models_gen.go │ └── resolver │ ├── resolver.go │ └── schema.resolvers.go ├── schema.graphql └── server.go
models_gen.go
スキーマに定義した Type
に対応する構造体が定義された。
コメントにあるように、自動生成ファイルはいじらないほうが吉。
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT. package model type Pet struct { ID *string `json:"id"` Name *string `json:"name"` } type User struct { ID *string `json:"id"` Name *string `json:"name"` }
resolver.go
ベースになる resolver。
resolver は Web アプリケーションアーキテクチャとしての MVC における Controller に近く、endpoint に対応するメソッドを実装している。余談だが、GraphQL モジュール自体薄く保つべきという指針が提唱されているので、Controller 同様多くの処理は持たせず軽い Validation などに留めるのがよい。
package resolver // This file will not be regenerated automatically. // // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct{}
schema.resolver.go
yml の resolver
に指定したスキーマファイル分だけ生成される。今回は schema.graphql
のみ指定したので、 schema.resolver.go
ファイル1つが生成された。
これらの自動生成ファイルで定義される xxxResolver
構造体は resolver.go
の Resolver
構造体をコンポジットする。
QueryResolver
の持つメソッドは GraphQL スキーマクエリのメソッドに対応するが、実装は空(panic)になっているため、独自に編集する必要がある(ので、もちろん編集は可能)。
package resolver // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. import ( "context" "fmt" "github.com/rennnosuke/tmp-gqlgen/graph/generated" "github.com/rennnosuke/tmp-gqlgen/graph/model" ) func (r *queryResolver) User(ctx context.Context, id *string) (*model.User, error) { panic(fmt.Errorf("not implemented")) } func (r *queryResolver) Pet(ctx context.Context, id *string) (*model.Pet, error) { panic(fmt.Errorf("not implemented")) } // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } type queryResolver struct{ *Resolver }
スキーマの更新・再生成
一旦初期自動生成後、スキーマの変更を反映したい場合は以下を実行する。
$ go run github.com/99designs/gqlgen
これで resolver
の上書きなしに、 model
や exec
に該当するファイルだけ更新できる。
サーバーの起動
コード自動生成の際、GraphQL API サーバーを起動する server.go
も自動で生成されている。
package main import ( "log" "net/http" "os" "github.com/99designs/gqlgen/graphql/handler" "github.com/99designs/gqlgen/graphql/playground" "github.com/rennnosuke/tmp-gqlgen/graph/generated" "github.com/rennnosuke/tmp-gqlgen/graph/resolver" ) const defaultPort = "8080" func main() { port := os.Getenv("PORT") if port == "" { port = defaultPort } srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &resolver.Resolver{}})) http.Handle("/", playground.Handler("GraphQL playground", "/query")) http.Handle("/query", srv) log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) log.Fatal(http.ListenAndServe(":"+port, nil)) }
ちなみに、 Server
構造体が Go 標準 net/http
の Handler
インタフェース実装のハンドラ関数 Handler
を持っているため、既存プロジェクトでも net/http
を使用していればすぐに組み込むことができる。
server.go
をそのまま起動すると API サーバーを起動できる。
$ go run server.go 2020/05/17 13:19:52 connect to http://localhost:8080/ for GraphQL playground
Request
実際にクエリを投げられるのを確認するため、Resolver メソッドを書き換える。
構造体のすべてのメンバ型がポインタなので、値を代入するとき少しもどかしい、、、
schema.resolver.go
func (r *queryResolver) User(ctx context.Context, id *string) (*model.User, error) { name := "Bob" return &model.User{ ID: id, Name: &name, }, nil }
上記実装後、 go run server.go
でサーバーを再起動すると user()
クエリが投げられるようになる。
Query
{ user(id:"user::1"){ name } }
Response
{ "data": { "user": { "name": "Bob" } } }
備考
個人の見解だが、自動生成される部分(特に exec
に該当する部分)は殆ど変更の入らない部分なのと、 model
resolver
も自動生成 + 取得処理の外部モジュール化で十分だと思ったので、Go で GraphQL API サーバーを実装する際は gqlgen で良きかな、と思った。
(graphql-goも試したが、Resolver 周りの実装が煩雑だったので心が折れた)
参考文献
CentOS7 dockerコンテナのsystemctlを使用可能にする
TL;DR
- CentOS7docker コンテナで systemctl が使えない
- コンテナ起動時、
--privileged
/sbin/init
を指定する
$ docker run -it --privileged --name centos -d centos:centos7 /sbin/init $ docker exec -it centos /bin/bash
概要
CentOS 7 の Docker イメージは、下記コマンドにて習得できる。
$ docker pull centos:centos7
コンテナを作成してシェルを実行すると、CentOS をホスト OS であるかのようにコマンドを使用できる。
$ docker run -it -d --name centos centos:centos7 $ docker exec -it centos /bin/bash
ただし、CentOS のサービス制御コマンド systemctl
を実行すると、以下のような表示が出て使用することができない。
$ Failed to get D-Bus connection: Operation not permitted
解決法
この問題を解消するためには、コンテナ起動時に --privileged
オプションを指定した上で、 /sbin/init
を起動する。
$ docker run -it --privileged --name centos -d centos:centos7 /sbin/init $ docker exec -it centos /bin/bash
参考文献
【Golang】net/httpのPath Parameterパース
net/http
の handleFunc()
handle()
に登録できるパスは Path Parameter を認識しない。例えば、他の Web フレームワークのように、下記 /products/:id
エンドポイント中の :id
を変数として取得することができない。
// :idを変数としてパースできない http.handleFunc("/products/:id", f)
Path Parameter を取得するためには、Path Parameter 抜きのエンドポイントへのリクエストを一旦ハンドルし、そのハンドラの中で Path Parameter をパースする必要がある。
func main() { http.handleFunc("/products/", handleProducts) http.ListenAndServe(":8000", nil) } func handleProducts(r *http.Request, w http.ResponseWriter) { sub := strings.TrimPrefix(r.URL.Path, "/products") _, id := filepath.Split(sub) if id != "" { // :idを使う... } }
/products/:id
リクエストへのハンドラを設定する際には、 /products
ではなく /products/
へハンドラを設定する。 /products
リクエストハンドラは /products/:id
リクエストを受け付けられないため。ただし /products/hoge
/products/hoge/fuga
などの適当なエンドポイントも受け付けてしまうので、404 を返すなど適宜処理する。
備考
Handle()
HandleFunc()
に登録できるパスパターンは、 net/http
中の構造体 ServeMux
のルールに従う。
パターンは、「/ favicon.ico」のような固定されたルート化されたパス、または「/ images /」のようなルート化されたサブツリーに名前を付けます(末尾のスラッシュに注意してください)。長いパターンは短いパターンよりも優先されるため、「/ images /」と「/ images / thumbnails /」の両方にハンドラーが登録されている場合、「/ images / thumbnails /」で始まるパスと前者のハンドラーが呼び出されます。 「/ images /」サブツリー内の他のパスのリクエストを受け取ります。
参考文献
【Golang】Golangの並列実行時の競合状態検出
-race
オプション
Go バイナリ実行時、 -race
オプションを指定することで競合状態のテストを実施することができる。具体的には
go -race run
go -race build
のようにコード実行時、バイナリビルド時に指定できる。
ただし実行可能な環境は linux/amd64、freebsd/amd64、darwin/amd64、windows/amd64 のみ。
Example
Golang の map の非スレッドセーフ性と排他制御の記事で掲載した、map に対する並行アクセスを実行する。
main.go
package main func main() { kvs := NewKeyValueStore() for i := 0; i < 10; i++ { go func(kvs *KeyValueStore) { kvs.set("key", "value") kvs.get("key") }(kvs) } } type KeyValueStore struct { m map[string]string } func NewKeyValueStore() *KeyValueStore { return &KeyValueStore{m: make(map[string]string)} } func (s *KeyValueStore) set(k, v string) { s.m[k] = v } func (s *KeyValueStore) get(k string) (string, bool) { v, ok := s.m[k] return v, ok }
上記コードは map がスレッドセーフで無いために、値書き込み時のデータの競合状態が発生する。このコードを -race
オプションとともに実行すると、競合状態が発生しうることに対する警告が吐き出される。
実行結果
$ go run -race main.go ================== WARNING: DATA RACE Write at 0x00c000088000 by goroutine 7: runtime.mapassign_faststr() /usr/local/go/src/runtime/map_faststr.go:202 +0x0 main.main.func1() /Users/rennnosuke/go/src/github.com/rennnosuke/rens-blog-codes/20200323/not_concurrent_safe/main.go:25 +0x71 Previous write at 0x00c000088000 by goroutine 6: runtime.mapassign_faststr() /usr/local/go/src/runtime/map_faststr.go:202 +0x0 main.main.func1() /Users/rennnosuke/go/src/github.com/rennnosuke/rens-blog-codes/20200323/not_concurrent_safe/main.go:25 +0x71 Goroutine 7 (running) created at: main.main() /Users/rennnosuke/go/src/github.com/rennnosuke/rens-blog-codes/20200323/not_concurrent_safe/main.go:8 +0x9c Goroutine 6 (finished) created at: main.main() /Users/rennnosuke/go/src/github.com/rennnosuke/rens-blog-codes/20200323/not_concurrent_safe/main.go:8 +0x9c ================== ================== WARNING: DATA RACE Write at 0x00c00008c088 by goroutine 7: main.main.func1() /Users/rennnosuke/go/src/github.com/rennnosuke/rens-blog-codes/20200323/not_concurrent_safe/main.go:25 +0x86 Previous write at 0x00c00008c088 by goroutine 6: main.main.func1() /Users/rennnosuke/go/src/github.com/rennnosuke/rens-blog-codes/20200323/not_concurrent_safe/main.go:25 +0x86 Goroutine 7 (running) created at: main.main() /Users/rennnosuke/go/src/github.com/rennnosuke/rens-blog-codes/20200323/not_concurrent_safe/main.go:8 +0x9c Goroutine 6 (finished) created at: main.main() /Users/rennnosuke/go/src/github.com/rennnosuke/rens-blog-codes/20200323/not_concurrent_safe/main.go:8 +0x9c ================== Found 2 data race(s) exit status 66
競合が発生しうる場合、上記のように競合が発生するコード上の箇所が表示される。競合検出は実行された処理のみに対して実施され、実行されない処理に競合の可能性があっても検出はされない。
競合状態にならない場合、特に出力はなく実行は終了する。
main.go
package main import ( "fmt" "sync" ) func main() { kvs := NewConcurrentKeyValueStore() for i := 0; i < 10; i++ { go func(kvs *ConcurrentKeyValueStore) { kvs.set("key", "value") kvs.get("key") }(kvs) } } type ConcurrentKeyValueStore struct { m map[string]string mu sync.RWMutex } func NewConcurrentKeyValueStore() *ConcurrentKeyValueStore { return &ConcurrentKeyValueStore{m: make(map[string]string)} } func (s *ConcurrentKeyValueStore) set(k, v string) { s.mu.Lock() defer s.mu.Unlock() s.m[k] = v } func (s *ConcurrentKeyValueStore) get(k string) (string, bool) { s.mu.RLock() defer s.mu.RUnlock() v, ok := s.m[k] return v, ok }
実行結果
$ go run -race main.go // 何も出力されない
パッケージインストール時の競合検出
コード実行・ビルド時だけでなく、外部パッケージをインストールするときにも競合検出を実施することができる。
go get -race [package]
go install -race [package]