Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【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 に対してコード経由で処理を実行できるエディタが開ける。

f:id:rennnosukesann:20210102173000p:plain

エディタは 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"}}}

f:id:rennnosukesann:20210102173009p:plain

参考文献

DynamoDB ローカル (ダウンロード可能バージョン) のセットアップ

【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::Querysql.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 つ。

  1. トランザクション呼び出しの定義 永続化処理などでトランザクション開始終了処理 Begin() Commit() RollBack() を呼ぶとき、それに合わせて SqlMock.ExpectBegin() SqlMock.ExpectCommit() SqlMock.ExpectRollback() を呼ぶ。

  2. 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

参考文献


  1. DBMS などの機能も含めて検証したい場合、モックではなく素直に検証用 DB 含めてテストしたほうが良いかも。モックの宿命

【Golang】GoでTCPソケット通信を実装する

ソケット通信

ソケット(BSD)- Wikipedia

ソケット通信とは、プログラムから見たときのネットワーク通信を抽象化する概念をさす。通信の終端をソケットと呼ばれるオブジェクトとみなし、ソケットに対して何かしらの入力を行うと、もう一方のソケットから出力される。

このモデルにおいて、ネットワーク階層モデル(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 でコードを自動生成する際に必要になる。

gqlgen.yml の詳しい定義はこちら

# 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.goResolver 構造体をコンポジットする。

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の上書きなしに、 modelexec に該当するファイルだけ更新できる。

サーバーの起動

コード自動生成の際、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/httpHandler インタフェース実装のハンドラ関数 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 周りの実装が煩雑だったので心が折れた)

参考文献

gqlgen - Github

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

参考文献

F.2.4. /SBIN/INIT プログラム

Docker privileged オプションについて

CentOS7 のコンテナで systemctl を使うための方法

【Golang】net/httpのPath Parameterパース

net/httphandleFunc() 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/amd64freebsd/amd64darwin/amd64windows/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]

参考文献