【Golang】Goで形態素解析する - mecab-golang
MeCab とは
MeCab: Yet Another Part-of-Speech and Morphological Analyzer
自然言語処理界隈では ChaSen と並び有名な形態素解析エンジン。
形態素解析とは、テキストデータを文法や単語の品詞情報(辞書)を元に言語の最小単位(形態素)へと分割し、各形態素の品詞などを判別すること。
ex. 裾野は長し赤城山
裾野 名詞,一般,*,*,*,*,裾野,スソノ,スソノ は 助詞,係助詞,*,*,*,*,は,ハ,ワ 長し 形容詞,自立,*,*,形容詞・アウオ段,文語基本形,長い,ナガシ,ナガシ 赤城山 名詞,固有名詞,地域,一般,*,*,赤城山,アカギヤマ,アカギヤマ
mecab-golang
Go から mecab を使用するライブラリ。mecab
の go wrapper。
なので使用する場合は mecab-golang
とは別に、MeCab や辞書をインストールする必要がある。
Install
Mecab のインストール
mecab
をインストール。
mecab
を扱うには、MeCab が形態素解析に使用する辞書が別途必要になる。
mecab-ipadic
はその MeCab 用の辞書の一つであり、日本語の単語情報(読み方や品詞など)を掲載している。1
$ brew install mecab $ brew install mecab-ipadic
mecab-ipadic-NEologd
のインストール
Web 上から逐一新語を登録してくれる辞書 mecab-ipanic では判断できない新語を(全てではないが)サポートする。 ↓ こんなかんじに、例えば名詞をより細かい品詞に分割して解釈してくれる。
default system dictionary | mecab-ipadic-NEologd ピット 星 人 | ピット 星人 この 世界 の 片隅 に | この世界の片隅に 辛 坊 | 辛坊 東海大 市原 望 洋 | 東海大市原望洋 方言 ラジオ 体操 | 方言 ラジオ体操 コイ キング | コイキング はだし の ゲン | はだしのゲン あさ パラ | あさパラ 雪 組 | 雪組
-n
で最新 ver 辞書をインストール
$ git clone git@github.com:neologd/mecab-ipadic-neologd.git $ cd mecab-ipadic-neologd $ ./bin/install-mecab-ipadic-neologd -n
mecab-golang
のインストール
$ export CGO_LDFLAGS="-L/{libフォルダへのパス}/lib -lmecab -lstdc++" $ export CGO_CFLAGS="-I/{includeフォルダへのパス}/include" $ go get github.com/bluele/mecab-golang
Usage
bluele/mecab-golang)サンプルコードを参考にした。
package main import ( "fmt" "strings" "github.com/bluele/mecab-golang" ) const BOSEOS = "BOS/EOS" func parseToNode(m *mecab.MeCab, text string) error { tg, err := m.NewTagger() if err != nil { return err } defer tg.Destroy() lt, err := m.NewLattice(text) if err != nil { return err } defer lt.Destroy() node := tg.ParseToNode(lt) for { features := strings.Split(node.Feature(), ",") if features[0] != BOSEOS { fmt.Printf("%s %s\n",node.Surface(), node.Feature()) } if node.Next() != nil { break } } return nil } func main() { m, err := mecab.New("-Owakati") if err != nil { panic(err) } defer m.Destroy() err = parseToNode(m, "すもももももももものうち") if err != nil { panic(err) } }
解説
func main()
mecab.New()
で MeCab モデルオブジェクトを生成する。
引数には元々の MeCab 実行バイナリの引数に指定できるものを渡すことができる。ここでは形態素解析の結果(後述の Tagger::Parse
で取得)を分かち書きで出力させるオプション -Owakati
を指定している。
他のオプションはここを参照。
parseToNode()
は第 2 引数にとる文字列を形態素解析・その結果を標準出力へ書き込む。
func parseToNode()
引数に取る MeCab
モデルオブジェクトと文字列から、形態素解析の結果を出力する。
以下で説明する関数やメソッドは MeCab c++ ライブラリ準拠のもののため、MeCab 本家の C/C++ライブラリを読めば問題なさそう。2
MeCab::NewTagger()
辞書オブジェクト Tagger
を生成する。
MeCab::NewLattice()
解析に必要なローカル変数を含むオブジェクト Lattice
を生成する。
生成時に解析対象となる文字列を渡す。
Tagger::ParseToNode()
Lattice
を引数に取ることで、 Lattice
が保持する文字列を解析し、木構造のオブジェクト Node
として解析結果を返す。
ちなみに parseToNode()
内では BOS/EOS( BOS は beginning of sentence で文頭、EOS は end of sentence で文末)
品詞タグのついた形態素情報以外を出力するようにしている。
Node::Feature()
Node
の解析結果を出力する。
例えば分かち書き指定の場合こんな感じ。
名詞,一般,*,*,*,*,すもも,スモモ,スモモ
Node::Surface()
単語の品詞を出力する。
実行結果
すもも 名詞,一般,*,*,*,*,すもも,スモモ,スモモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ も 助詞,係助詞,*,*,*,*,も,モ,モ もも 名詞,一般,*,*,*,*,もも,モモ,モモ の 助詞,連体化,*,*,*,*,の,ノ,ノ うち 名詞,非自立,副詞可能,*,*,*,うち,ウチ,ウチ
独自フォーマットで形態素解析の結果を取得する
Tagger::ParseToNode
で取得する Node
からは特定のフォーマットで結果が出力できるが、 mecab.New()
でフォーマットを指定することで、 Tagger::Parse
の文字列結果を変更することができる。
分かち書き
m, err := mecab.New("-Owakati" )
すもも も もも も もも の うち
フォーマット
-F
オプションを使用すると、下記の出力フォーマットを使用して自由に出力を切り替えられる。
出力フォーマット
ex.
m, err := mecab.New("-F%m(素性ID:%h)\\n")
すもも(素性ID:38) も(素性ID:16) もも(素性ID:38) も(素性ID:16) もも(素性ID:38) の(素性ID:24) うち(素性ID:66) EOS
参考文献
MeCab: Yet Another Part-of-Speech and Morphological Analyzer
mecab-ipadic-neologd/README.ja.md at master · neologd/mecab-ipadic-neologd
MeCab: Yet Another Japanese Dependency Structure Analyzer
bluele/mecab-golang: A golang wrapper for mecab.
IPADIC(IPA 辞書)とはなにものか? - ぱらめでぃうす
-
元々は別の形態素解析エンジンである ChaSen に使用された辞書を、MeCab 用に更に変更を加えたもの。ipa とついているのは、その品詞フォーマットが情報処理振興事業協会(IPA)で設定された IPA 品詞体系(THiMCO97)に基づいているため。参考:http://parame.mwj.jp/blog/0209↩
-
この C/C++ライブラリドキュメントでは Tagger によるシングルスレッド環境向けサンプルと Tagger/Model(NewMecab 返戻値型に対応)/Lattice によるマルチスレッド環境向けサンプルがある。
mecab-golang
では Go を使用する以上、goroutine によるマルチスレッド環境の可能性を前提にライブラリが組まれていると思われ、そのため Tagger::Parse/ParseToNode は Lattice を引数に取っている。↩
【Golang】Golang:logパッケージを読む
log
パッケージを読みました。
src/log/log.go - The Go Programming Language
log
Go の log
はロギング処理に使用するパッケージ。
log
パッケージはテストコードを除けば log.go
のみで構成されており、log.go
も 400 数行からなるシンプルな構成となっている。
( syslog
が log
パッケージ配下に置かれているが、それとは分けて話す )
log
パッケージ概要
// Package log implements a simple logging package. It defines a type, Logger, // with methods for formatting output. It also has a predefined 'standard' // Logger accessible through helper functions Print[f|ln], Fatal[f|ln], and // Panic[f|ln], which are easier to use than creating a Logger manually. // That logger writes to standard error and prints the date and time // of each logged message. // Every log message is output on a separate line: if the message being // printed does not end in a newline, the logger will add one. // The Fatal functions call os.Exit(1) after writing the log message. // The Panic functions call panic after writing the log message. package log
- ロギングに使用する Logger 型と、それに紐づくメソッド(Formatter など)を定義している
- Logger を明示的に生成してロギングするか、ヘルパ=関数経由で標準 Logger によるロギングができる
- ヘルパー関数を使用すると、標準 Logger がログメッセージに日付・時刻を付加する
- Logger 経由のロギングは以下の特徴を持つ
- 1 ログにつき 1 行(カスタム Logger でなければ)
- Fatal 系関数はロギング後 os.Exit(1)を呼ぶ
- Panic 系関数はロギング後 panic を呼ぶ
Logger 構造体
// A Logger represents an active logging object that generates lines of // output to an io.Writer. Each logging operation makes a single call to // the Writer's Write method. A Logger can be used simultaneously from // multiple goroutines; it guarantees to serialize access to the Writer. type Logger struct { mu sync.Mutex // ensures atomic writes; protects the following fields prefix string // prefix on each line to identify the logger (but see Lmsgprefix) flag int // properties out io.Writer // destination for output buf []byte // for accumulating text to write }
Logger
構造体は log
パッケージが提供するロギング機能を実現するための構造体。
mu
:sync.Mutex
値。goroutine による並行処理上でも排他制御しつつロギングするために使用する。prefix
: ログ行の先頭に付与する文字列。ただし、Lmsgprefix
をflag
に指定するとメッセージの先頭に付与される。flag
: ログの接頭辞を制御するフラグ。指定できるフラグは以下。ビットフラグになっており、 OR 演算で結合して複数指定できる。 ex.Ldate | Lmicroseconds
out
:io.Writer
実装型の値。ログの出力先。buf
: ログ書き込み文字列のバッファ。
const ( Ldate = 1 << iota // ローカルタイムゾーンの日時を次のフォーマットで表示: 2009/01/23 Ltime // ローカルタイムゾーンの時刻を次のフォーマットで表示: 01:23:23 Lmicroseconds // ローカルタイムゾーンの時刻を次のフォーマットで表示: 01:23:23.123123. assumes Ltime. Llongfile // パス/ファイル名と行数を表示: /a/b/c/d.go:23 Lshortfile // ファイル名と行数を表示、Llongfileを上書きする: d.go:23. LUTC // Ldate, Ltimeが設定されていた場合、タイムゾーンをUTCとする Lmsgprefix // prefixをログ先頭ではなくメッセージ先頭に移す LstdFlags = Ldate | Ltime )
Logger を明示的に作成して使用する
log
パッケージには Logger 値参照を取得するヘルパー関数 New
が提供されている。
引数の詳細は上記参照。
// New creates a new Logger. The out variable sets the // destination to which log data will be written. // The prefix appears at the beginning of each generated log line, or // after the log header if the Lmsgprefix flag is provided. // The flag argument defines the logging properties. func New(out io.Writer, prefix string, flag int) *Logger { return &Logger{out: out, prefix: prefix, flag: flag} }
Logger public メソッド
l := log.New(os.Stdout, "prefix: ", log.Ldate | log.Lmicroseconds | log.LUTC) l.Println("Hello, Logger.")
Logger
が使用できる出力系関数は以下。
func (l *Logger) Printf(format string, v ...interface{}) func (l *Logger) Print(v ...interface{}) func (l *Logger) Println(v ...interface{}) func (l *Logger) Fatal(v ...interface{}) func (l *Logger) Fatalf(format string, v ...interface{}) func (l *Logger) Fatalln(v ...interface{}) func (l *Logger) Panic(v ...interface{}) func (l *Logger) Panicf(format string, v ...interface{}) func (l *Logger) Panicln(v ...interface{})
これらの関数全ては、内部で Logger::Output
関数を呼び出している。
Logger::Output
Logger::Output
は、各種出力系関数から呼ばれる汎用出力関数。
呼び出すごとに1行のログを出力する。
// Output writes the output for a logging event. The string s contains // the text to print after the prefix specified by the flags of the // Logger. A newline is appended if the last character of s is not // already a newline. Calldepth is used to recover the PC and is // provided for generality, although at the moment on all pre-defined // paths it will be 2. func (l *Logger) Output(calldepth int, s string) error { // ... }
Logger
の sync.Mutex
を使用して排他制御をしているため、ロギングはスレッドセーフ(goroutine セーフ?)。
l.mu.Lock() defer l.mu.Unlock() // ... _, err := l.out.Write(l.buf) return err
Logger.flag
に Lshortfile
Llongfile
が設定されている場合、ファイル名・行数を取得するため、ロックを外して runtime.Caller
を呼ぶ。
if l.flag&(Lshortfile|Llongfile) != 0 { // Release lock while getting caller info - it's expensive. l.mu.Unlock() var ok bool _, file, line, ok = runtime.Caller(calldepth) if !ok { file = "???" line = 0 } l.mu.Lock() }
Caller
は runtime
パッケージの関数で、ゴルーチンスタック中の関数呼び出し情報を取得できる。この関数呼び出しの情報の中にロギング時のファイル・行数情報が含まれていて、 Output
ではそれを使う。
package runtime import "runtime/internal/sys" // Caller reports file and line number information about function invocations on // the calling goroutine's stack. The argument skip is the number of stack frames // to ascend, with 0 identifying the caller of Caller. (For historical reasons the // meaning of skip differs between Caller and Callers.) The return values report the // program counter, file name, and line number within the file of the corresponding // call. The boolean ok is false if it was not possible to recover the information. func Caller(skip int) (pc uintptr, file string, line int, ok bool) { rpc := make([]uintptr, 1) n := callers(skip+1, rpc[:]) if n < 1 { return } frame, _ := CallersFrames(rpc).Next() return frame.PC, frame.File, frame.Line, frame.PC != 0 }
フラグ指定による接頭辞を付与するために、 formatHeader
を呼ぶ。
l.buf = l.buf[:0]
l.formatHeader(&l.buf, now, file, line)
formatHeader
は Logger.flags
ビットフラグに従い接頭辞を指定する。
接頭辞は以下の優先順位で付与される。
Logger.prefix
に指定した文字列- 日時・時刻
- ファイル名・行数
- (
Lmsgprefix
をLogger.prefix
に設定した場合 )Logger.prefix
に指定した文字列
// formatHeader writes log header to buf in following order: // * l.prefix (if it's not blank and Lmsgprefix is unset), // * date and/or time (if corresponding flags are provided), // * file and line number (if corresponding flags are provided), // * l.prefix (if it's not blank and Lmsgprefix is set). func (l *Logger) formatHeader(buf *[]byte, t time.Time, file string, line int) { // ... }
あとは Logger.out
に文字列を書き込む。
文字列末尾に改行が含まれていなければ追加する。
l.buf = append(l.buf, s...) if len(s) == 0 || s[len(s)-1] != '\n' { l.buf = append(l.buf, '\n') } _, err := l.out.Write(l.buf) return err
Logger をヘルパー関数経由で使用する
log
パッケージクライアントは Logger
型の値を生成しなくても、下記関数でロギング機能を使用することはできる。
func Printf(format string, v ...interface{}) func Print(v ...interface{}) func Println(v ...interface{}) func Fatal(v ...interface{}) func Fatalf(format string, v ...interface{}) func Fatalln(v ...interface{}) func Panic(v ...interface{}) func Panicf(format string, v ...interface{}) func Panicln(v ...interface{})
これらの関数は共通のローカル Logger
値である std
を使用する。
std
の出力先は os.Stderr
であり、接頭辞として日付・時刻がつく。
var std = New(os.Stderr, "", LstdFlags)
上記関数群は内部で std.Output
を呼び、ロギング処理を実行する。
std
経由で Logger
メソッドを呼び出さない理由は不明(ユースケース別にあえて分けた?)。
// Printf calls Output to print to the standard logger. // Arguments are handled in the manner of fmt.Printf. func Printf(format string, v ...interface{}) { std.Output(2, fmt.Sprintf(format, v...)) }
参考文献
【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