- 投稿日:2019-03-05T22:19:11+09:00
go言語でhtmlから取得したデータをjson形式で保存する
Goの勉強録
昨日あたりからGoに対するエンジニアの成長曲線の傾きが正に大きい部分に入ったのでは?と思うくらいGoを書くのが開発環境構築したてのころと比べて苦じゃ無くなりました。以前はエラーが出ると何処エラーしてるんやとか、うーんわからんみたいな感情があったのですが、結構早くエラーにならないように記述する事ができるようになりました。(潜在的なバグなどはわかりませんけれど。。)
昨日JavaScriptから送られて来るリクエストからデータを取得してコンソールに出力することができ、取得したこのデータを煮るなり焼くなりと言ったのでまずは手始めに、JSON形式で外部ファイルに保存しようと思いました。
あっさりJSONファイルにデータを保存することができたのでブログに残しておきます。ちなみに過去のGo勉強録記事は下から飛べるようにしています。
過去の記事
本題
go言語はPHPやruby on rails、Node.js(サーバサイド)と言った言語と同じようにサーバサイドのプログラムを記述することができます。サーバサイドのプログラムを記述できるということは、サーバから必要なデータを引っ張ってきたり、データを書き込んだりするのが主な動作になります。
そこで今回はデータを書き込むという如何にもサーバサイドの動作を実装しました。ちなみにワークスペースの階層は以下のようになっています。Dataの中にdata.jsonが作られるようにします。(新幹線で作業をしたのでAngularのフレームワークをHTMLの中にぶっこんでいます。)
昨日のものを少しカスタマイズした感じですね。workspace/ ├ Data/ │ ├ HTML/ │ ├ index.html │ ├ test.js │ ├ Angular.min.js │ ├ POST/ │ ├ Process.go │ ├ Host.goソースコード
Host.go
サーバを立てて、来るリクエストに対してデータを取得し、jsonに書き込むという処理が記述されています。Host.gopackage main import ( "encoding/json" "fmt" "log" "net/http" "os" "./Post" ) func main() { HostingServer() } // Json形式でデータを保存するようにHandleを定義 func SaveJson() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // ファイルの生成 file, err := os.Create("./Data/data.json") if err != nil { fmt.Println("ファイルの生成に失敗しました") } defer file.Close() // Process.goからGetDataを呼び出し、リクエストのデータをData形式で取得する var data = Post.GetData(w, r) // 出力用にData形式のdataをjson形式にencode encodedjson, _ := json.Marshal(&data) // 正常に生成されたファイルに書き込み file.Write(([]byte)(string(encodedjson))) // エンコードされたデータをコンソールに表示 fmt.Println(string(encodedjson)) }) } // サーバを立てる処理 func HostingServer() { // Handlerの追加 SaveJson() dir, _ := os.Getwd() // サーバを立てる為にstripprefixを設定 fileserver := http.FileServer(http.Dir(dir + "/HTML/")) stripprefix := http.StripPrefix("/datatest/", fileserver) http.Handle("/datatest/", stripprefix) // serve :8080 log.Fatal(http.ListenAndServe(":8080", nil)) }Process.go
リクエストのデータをData形式で取得し、それを返す処理をしています。Process.gopackage Post import ( "encoding/json" "fmt" "net/http" "reflect" ) type Data struct { Name string Age int Gender string } func GetData(w http.ResponseWriter, r *http.Request) Data { // parse r.ParseForm() var data = Data{} decoder := json.NewDecoder(r.Body) decoder.Decode(&data) fmt.Println(reflect.TypeOf(data)) return data }index.html
表示用のスクリプトindex.html<!DOCTYPE html> <html ng-app="idx"> <head> <title>FileIO</title> <script src="./Angular.min.js"></script> <script src="./test.js"></script> </head> <body ng-controller="form"> <input type="text" ng-model="name"> <br /> <input type="number" ng-model="age"> <br /> 男:<input type="radio" ng-model="gender" value="man"> <br /> 女:<input type="radio" ng-model="gender" value="woman"> <br /> <input type="button" ng-click="req()" value="送信"> </body> </html>test.js
index.htmlでボタンを押された時に呼び出すイベントリスナーを定義しています。test.jsvar idx = angular.module("idx",[]) idx.controller("form",function($scope,$http){ $scope.req = function(){ // 送信用のデータ data = { name: $scope.name, age: $scope.age, gender: $scope.gender } // POSTでhttp通信 $http({ method: 'POST', url: '../Host.go', contentType: 'application/JSON', data: data }).then(function DoneCallback(res){ console.log("通信成功"); console.log(res) },function failCallback(res){ console.log("通信失敗"); }); } })
go run Host.go
を実行し、http://localhost:8080/datatest/
にアクセスすると
簡易的な入力フォームができています。
そこに値を入力しボタンを押すとData/data.json
が生成され、中身はdata.json{"Name":"test","Age":20,"Gender":"woman"}とJSON形式で保存されます。
まとめ
go側で無事データをJSON形式で保存する事ができました。(ようやく面白くなってきたぞ(-ω- )o)
次はファイルの読み込みなどを書くと思います。後々DBなどの接続や、並列処理と言った所の記事がかけていけたらなと思っています!(勉強することは山積みですな)
まだまだ、go初心者なので間違っている部分があれば、気軽にコメントを描いてください!
- 投稿日:2019-03-05T18:43:01+09:00
GO言語 Macインストールメモ
参考URL
公式サイト:https://golang.org
環境
macOS High Sierra バージョン10.13.6
GO 1.12インストール
$ brew install go ==> Downloading https://homebrew.bintray.com/bottles/go-1.12.high_sierra.bottle.tar.gz ######################################################################## 100.0% ==> Pouring go-1.12.high_sierra.bottle.tar.gz � /usr/local/Cellar/go/1.12: 9,787 files, 449.7MB brew install go 35.23s user 41.35s system 53% cpu 2:23.62 total $ go version go version go1.12 darwin/amd64動作確認
公式サイトのソースを動かしてみる
$vi hello.go // You can edit this code! // Click here and start typing. package main import "fmt" func main() { fmt.Println("Hello, 世界") } $go run hello.go Hello, 世界ビルド
$go build hello.go $ls hello hello.go $./hello Hello, 世界
- 投稿日:2019-03-05T10:09:25+09:00
Go初心者が書くarXiv APIを使って論文リストから論文を取ってくるアプリ
はじめに
自分の分野の有名な学会が開かれました.Accepted Paperのページがあります.とりあえずタイトルだけ見て,面白そうなやつ/自分の立場を脅かしようなやつをチェックしなきゃ...
みたいなことになる人もいるかもしれません.(まぁ普段からアンテナを貼っておけばこんなことにはならないのかもしれませんが,僕みたいにSNSが嫌いな人はこうなります)
とりあえず,タイトルで検索してarXivでダウンロードしてくるわけですが,ポチポチするのは面倒なので途中でやめてしまいます.そこでAccepted Paperのリストからほしい論文のタイトルだけコピペ(全部ブラウザから調べるよりは多少まし)して,タイトルリストからダウンロードしてくれるアプリがあったらいいな,ということで作りました.
僕のモチベーションはあくまでGoの勉強です.何を作ったの?
dpxgo
というクロスプラットホーム CLIアプリ (コマンド) を作りました.
リストにある論文のアブストラクトとpdfをarXiv APIをつかってダウンロードします.
arXivにない場合は自分で調べてください.
ちなみにdpxgo
はDownload Papers from arXiv with GO app.の略です.特徴
- Windows, mac, Linuxに対応しています.(Windowsとmacは動作未確認)
- 実行ファイルで配布しているので,ダウンロードすればすぐに利用できます.
- アブストラクトだけの保存も可能です.
- 関連研究のサーベイをするときに便利かもしれません.
- Goroutineによって並列処理しているので,ほしい論文がたくさんあっても,多分ある程度速くダウンロードできます.
似たようなの
- とりあえず
arXiv downloader
とかで調べたらいっぱい出てきます.
- arXiv APIを使いこなすためのライブラリ紹介
- 自動でArxivから論文を取得して、日本語に直してLineに投稿すれば、受動的に最新情報が収集できるかも!?
- arXiv APIのPythonでの取得
好きなのを使ってください.みんな素晴らしいと思います.
どう使うの?
まず,gitlabからダウンロードしてください.
使用例
dpxgo paperlist
: paperlistにある論文(アブストラクトとpdf)をダウンロードdpxgo -d out/ paperlist
: 保存先ディレクトリの指定dpxgo -w 20 paperlist
: 並列数(Goroutine)の指定dpxgo -a paperlist
: アブストラクトだけ取ってくる.paperlistの書き方は以下の通りです.Plain textならファイル名は何でもいいです(多分).
TITLE of PAPER1 TITLE of PAPER2 TITLE of PAPER3 TITLE of PAPER4タイトルごとに改行で区切ってください.空の行があっても読み飛ばします.
オプション
-d
: 保存先の指定 (default ./)-w
: Goroutineの並列数 (default 10)-a
: アブストラクトのみ保存 (default false)Goコード
全文は,gitlabを参照してください.特に大事そうな部分を説明します.
メイン関数
func main() { // parse command line variables parseArg() // download papers with ?Goroutine? ch := make(chan string) wg := sync.WaitGroup{} // make workers for i:=0; i<NumWorkers; i++{ wg.Add(1) go downloadPapers(&wg, ch) } paperTitles := parsePaperList(ListPath) for _, fileName := range paperTitles { ch <- fileName } close(ch) wg.Wait() }並列化の方法は,ここを参考にしています.
コマンドライン引数のパース
func parseArg() { targetDir := flag.String("d", "./", "directory path to save") numWorkers := flag.Int("w", 10, "number of workers") onlyAbst := flag.Bool("a", false, "only save abstract") flag.Parse() TargetDir = *targetDir NumWorkers = *numWorkers OnlyAbst = *onlyAbst // is TargetDir exist? if _, err := os.Stat(TargetDir); os.IsNotExist(err) { panic(TargetDir+" is not exist") } endStr := string(TargetDir[len(TargetDir)-1]) if endStr != "/" { TargetDir = TargetDir+"/" } args := flag.Args() if flag.NArg() == 0 { fmt.Println("dpxgo [options] list") return } else { // it does not support multiple paper lists ListPath = args[0] } }Goでは
flag
によってコマンドライン引数をパースするみたい(これ以外にもやり方はある)ですが,この処理をfunc main()
でやるとなんか嫌なので,関数にしてまとめて,コマンドライン引数で扱いたい変数は全てグローバル変数にしました.
すこし,苦戦したのがflag.Parse()
の位置です.flag.Type
関数のすぐあとで行うべきです.つまり,グローバル変数に代入した後でもエラーにはならないのですが,コマンドライン引数がすべてdefaultのものになります.arXiv APIの使い方
UrlHead = "http://export.arxiv.org/api/query?search_query=" UrlFoot = "&start=0&max_results=1" // toriaezu 1 ken dake strQuery := url.QueryEscape(paperTitle) resp, _ := http.Get(UrlHead+strQuery+UrlFoot) defer resp.Body.Close() html, _ := ioutil.ReadAll(resp.Body)
downloadPapers (*wg, ch)
の中のこの4行の部分でarXiv APIを利用しています.UrlFoot
のmax_results
を変更するとたくさんの論文がヒットして,キーワードで検索したいときは便利だと思います.でも今回の目的はタイトルリストからの論文取得なので,結果件数は1件で十分です.
ここでhtml
は[]byte
型の変数です.ここから必要な情報をパースしていきます.ちなみにstring
型に変換すると普通のhtml形式の文字列です.HTMLのタグの中身の取得
func isTagExist(html []byte, tagStr string) (bool) { regStr := `<`+tagStr+`>[\s\S]*</`+tagStr+`>` regTag := regexp.MustCompile(regStr) return regTag.Match(html) } func fetchTag(html []byte, tagStr string) ([]byte) { // <tag>[\s\S]</tag> regStr := `<`+tagStr+`>[\s\S]*</`+tagStr+`>` regTag := regexp.MustCompile(regStr) content := regTag.FindSubmatch(html)[0] content = bytes.Replace(content, []byte("<"+tagStr+">"), []byte(""), -1) content = bytes.Replace(content, []byte("</"+tagStr+">"), []byte(""), -1) return content } // short version func isTagExist(html []byte, tagStr string) (bool) { return regexp.MustCompile(`<`+tagStr+`>[\s\S]*</`+tagStr+`>`).Match(html) } func fetchTag(html []byte, tagStr string) ([]byte) { content := regexp.MustCompile(`<`+tagStr+`>[\s\S]*</`+tagStr+`>`).FindSubmatch(html)[0] return regexp.MustCompile(`<`+tagStr+`>|</`+tagStr+`>`).ReplaceAll(content, []byte(``)) }この2つの関数を実装しました.Goのライブラリにgoqueryというものがあり,これが便利らしいのですが,この存在に気づいたのは書いている途中だったため,見なかったことにしました.
isTagExist
ではhtml形式の[]byte
型変数がtagStr
タグを含むかどうかをチェックします.arXiv APIでは,検索がヒットしたとき,<entry>
タグがつくので,この関数で,arXivに論文が投稿されているかどうかをチェックします.
fetchTag
ではtagStr
タグの中身を取ってきます(タグは含まない).論文のタイトルやアブストラクトは先に取得したhtml
変数に含まれている(もちろん検索がヒットした場合に限る)ので,この関数で取ってきます.
ただし,このfetchTag
関数ではタグが入れ子になっている場合は,おそらく思った通りに動作しません.しかしながら,arXiv APIのレスポンスにはそのような構造はありませんのでこれで十分です.タイトルの一致判定
func matchTitle(titlePred string, titleTrue string) (bool) { sp := strings.Split(titlePred, " ") st := strings.Split(titleTrue, " ") matchNum := 0.0 for _, tp := range sp { for _, tt := range st { if tp == tt { matchNum += 1.0 break } } } if threshold:=0.7; matchNum/float64(len(st))>threshold { return true } else { return false } }すこし使って気づいたのですが,arXiv APIの検索は,ある程度タイトルが一致している論文を提示するため,結構違う論文がダウンロードされることがあったので,タイトルの一致判定を行う関数を追加しました.これは,以下の式でタイトルの類似性を測っています(類似性が大きいほど似ている).
\text{similarity} = \frac{|\text{{検索されたタイトルの単語集合}}\cap\text{{指定したタイトルの単語集合}}|}{|\text{{指定したタイトルの単語集合}}|}まぁ,Recallというか,コサイン距離っていうやつですかね.
pdfファイルの保存
// save pdf (error ハンドリングは省略) urlPdf := fetchTag(fetchTag(html, `entry`), `id`) urlPdf = bytes.Replace(urlPdf, []byte("abs"), []byte("pdf"), 1) urlPdf = bytes.Replace(urlPdf, []byte("http"), []byte("https"), 1) resp, err = http.Get(string(urlPdf)+".pdf") defer resp.Body.Close() pdf, _ := ioutil.ReadAll(resp.Body) err = ioutil.WriteFile(TargetDir+string(title)+".pdf", pdf, 0666)pdfの保存は
html
の取得と同じ要領でhttp.Get()
を使えばできます.おわりに
とりあえず,動いてかつ自己満足にひたれるものができました.
一番苦戦したのはタグのパターンマッチを行うための正規表現でした(普段正規表現をまったくつかないため).
- 投稿日:2019-03-05T09:29:27+09:00
golangのembeddingによる継承(っぽい操作)で今更ながら思ったこと
今更ながら、こういう仕様だったのか・・・とハマったのでメモ。
goってextendがない代わりにembeddingによる継承っぽいことができますよね。
その際の継承先structの初期化で、
えっ!?この操作できなかったんだ・・・
と今更ながら知ったのでメモです。まずエラーになるパターン
package main import ( "fmt" ) // 基のstruct。NameとDescriptionを持っている type Base struct { Name string Description string } // 拡張?したstruct。ExtraInformationというプロパティを足した type Extended struct { Base ExtraInformation string } func main() { ext := Extended{ Name: "name", Description: "description", ExtraInformation: "extra", } fmt.Printf("Struct: %#v", ext) }これだと、
cannot use promoted field Base.Description in struct literal of type Extended
というエラーになります。embeddingされている方のフィールドを
promoted field
と呼ぶようですね。うまくいくパターン
こうしないと駄目らしい。
package main import ( "fmt" ) // 基のstruct。NameとDescriptionを持っている type Base struct { Name string Description string } // 拡張?したstruct。ExtraInformationというプロパティを足した type Extended struct { Base ExtraInformation string } func main() { ext := Extended{ Base: Base{ Name: "name", Description: "description", }, ExtraInformation: "extra", } fmt.Printf("Struct: %#v", ext) }もしくはこう。struct literal なるものから脱出さえしていればどうにでもなる系。
package main import ( "fmt" ) type Base struct { Name string Description string } type Extended struct { Base ExtraInformation string } func main() { ext := Extended{} ext.Name = "name" ext.Description = "description" ext.ExtraInformation = "extra" fmt.Printf("Struct: %#v", ext) }普通にPythonとかで書いている人にとっては意外な仕様だったのでメモしておきました。
- 投稿日:2019-03-05T09:27:47+09:00
cgo で構造体配列を引数に取る C 関数を呼び出す方法の一例 (Go pointer to Go pointer エラー回避方法)
環境
- go: 1.12 linux/amd64
問題の概要
C では関数の引数として構造体を渡す場合, 通常ポインタで渡す. この構造体のメンバーの中にポインタ変数が含まれている場合に cgo でこの関数を何も考えずに直接呼び出すと “Go pointer to Go pointer” エラーが発生する.
具体的な例を挙げてみる. 例えば下記のような C のコードがあったとして, cgo から printMyStructs() 関数を呼び出したいとする.
#include <stdlib.h> #include <stdio.h> #include <string.h> typedef struct mystruct { int i; int* pi; } mystruct; /* cgo から呼び出したい関数 */ void printMyStructs(int num, mystruct* strctList) { int j; for(j = 0; j < num; j++) { printf("%d: i=%d, pi=%d\n", j, strctList[j].i, *(strctList[j].pi)); } }このとき, GO 側のコードは何も考えずに書くと例えば下記のようになるのだが...
length := 10 strctArray := make([]C.mystruct, length) // building strctArray pi_ := C.int(1000) strctArray[0].i = C.int(10) strctArray[0].pi = (*C.int)(unsafe.Pointer(&pi_)) .... C.printMyStructs(C.int(length), (*C.mystruct)(unsafe.Pointer(&strctArray[0])))これをコンパイルすると, 末尾の C.printMyStructs() を呼び出す箇所で下記のような Go pointer to Go pointer エラーとなる.
$ go run cstructarray.go panic: runtime error: cgo argument has Go pointer to Go pointer goroutine 1 [running]: main.main.func1(0x1, 0xc000074010, 0x1, 0x1) /home/endoh/go/src/test/cstructarray.go:31 +0x52 main.main() /home/endoh/go/src/test/cstructarray.go:31 +0xa3 exit status 2構造体単体で渡す場合ならばポインタ渡しを諦めて実体渡しができるようなラッパー関数を C 側で用意するという手もあるが, 構造体の配列を渡す場合はそうもいかない.
解決方法の一例
C 側にヘルパー関数を 2 つ用意して, Go からはこれらを使いつつ最終的に目的の C 関数を呼び出すという手順を取った.
C 側の準備
C 側には次の 2 つの関数を準備する.
(1) 構造体のメモリ確保を行ってポインタを返す関数
mystruct* allocMystruct(int num) { return malloc(num * sizeof(mystruct)); }(2) 構造体配列の任意の要素に引数から渡された構造体をコピーする関数
/* offset が配列の何個目かを表す */ void convertMystructData(mystruct* strctArray, int offset, mystruct src) { memcpy(strctArray + offset, &src, sizeof(mystruct)); }Go 側の手順
Go 側では次のような手順を踏む.
1. make で C.mystruct の配列を作成
2. 1 で作成した配列の各要素に値を入れる
3. C の「構造体のメモリ確保を行ってポインタを返す関数」を呼び出す
4. C の「構造体配列の任意の要素に引数から渡された構造体をコピーする関数」を上記 2 と 3 で得た変数を引数として呼び出す3 で得たポインタは C ポインタなので構造体のメンバーに Go ポインタが含まれていても Go pointer to Go pointer エラーは発生しない. やっていることは実体渡しと同様, 構造体配列をせっせとコピーしているので全くもってスマートとは言えないが, とりあえずこれで動作する.
- go側のコード
length := 10 strctArray := make([]C.mystruct, length) // これは GO pointer // building strctArray pi_ := C.int(1000) strctArray[0].i = C.int(10) strctArray[0].pi = (*C.int)(unsafe.Pointer(&pi_)) ... cStrctArray := C.allocMystruct(C.int(length)) // こっちは C pointer defer C.free(unsafe.Pointer(cStrctArray)) for i, strct := range strctArray { C.convertMystructData(cStrctArray, C.int(i), strct) } C.printMyStructs(C.int(length), cStrctArray)実行結果は下記の通り. 問題なく動いている模様.
$ go run cstructarray.go 0: i=10, pi=1000 (以下略)コード全体
コード全体も載せておきます.
cstructarray.gopackage main //#include <stdlib.h> //#include <stdio.h> //#include <string.h> // //typedef struct mystruct { // int i; // int* pi; //} mystruct; // //void printMyStructs(int num, mystruct* strctList) { // int j; // for(j = 0; j < num; j++) { // printf("%d: i=%d, pi=%d\n", j, strctList[j].i, *(strctList[j].pi)); // } //} // //mystruct* allocMystruct(int num) { // return malloc(num * sizeof(mystruct)); //} // //void convertMystructData(mystruct* strctArray, int offset, mystruct src) { // memcpy(strctArray + offset, &src, sizeof(mystruct)); //} // import "C" import ( "unsafe" ) func main() { length := 1 strctArray := make([]C.mystruct, length) pi_ := C.int(1000) strctArray[0].i = C.int(10) strctArray[0].pi = (*C.int)(unsafe.Pointer(&pi_)) cStrctArray := C.allocMystruct(C.int(length)) defer C.free(unsafe.Pointer(cStrctArray)) for i, strct := range strctArray { C.convertMystructData(cStrctArray, C.int(i), strct) } C.printMyStructs(C.int(length), cStrctArray) }感想
cgo つらい
- 投稿日:2019-03-05T08:22:00+09:00
Go言語のモック(gomock)を触ってみた
はじめに
Goでモックを使ったテストを書く際によく使用されるgomockを触ってみました。
モックとは何かなどの説明はこの記事では対象外です。gomockとは
https://github.com/golang/mock
Goのモックライブラリ。
モックにできるのはインターフェースとそのメソッドのみであり、なんでもモックにできるわけではない。
testing(標準パッケージ)と組み合わせて使用するケースが多い。インストール
$ go get github.com/golang/mock/gomock $ go install github.com/golang/mock/mockgen使ってみる
モックにされる対象のインターフェース
今回は、Barというメソッドを持つFooインターフェースを例にします。
foo.gotype Foo interface { Bar(x int) int }モックを自動生成する
mockgenコマンドでモックファイルを自動生成します。
-sourceオプションでモック対象のファイルを指定し、-destinationオプションでモックファイルの出力先を指定します。
mockgenコマンドには他にも色々オプションがあるみたいです。詳しくはこちら
mockファイルはmock_<package_name>/
ディレクトリ配下にmock_<package_name>.go
という名前で作るのが通例ぽいです。それでは作っていきましょう。
$ mkdir mock_foo $ mockgen -source foo.go -destination mock_foo/mock_foo.go生成されたファイルが以下です。
mock_foo/mock_foo.go// Code generated by MockGen. DO NOT EDIT. // Source: foo.go // Package mock_foo is a generated GoMock package. package mock_foo import ( gomock "github.com/golang/mock/gomock" reflect "reflect" ) // MockFoo is a mock of Foo interface type MockFoo struct { ctrl *gomock.Controller recorder *MockFooMockRecorder } // MockFooMockRecorder is the mock recorder for MockFoo type MockFooMockRecorder struct { mock *MockFoo } // NewMockFoo creates a new mock instance func NewMockFoo(ctrl *gomock.Controller) *MockFoo { mock := &MockFoo{ctrl: ctrl} mock.recorder = &MockFooMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use func (m *MockFoo) EXPECT() *MockFooMockRecorder { return m.recorder } // Bar mocks base method func (m *MockFoo) Bar(x int) int { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Bar", x) ret0, _ := ret[0].(int) return ret0 } // Bar indicates an expected call of Bar func (mr *MockFooMockRecorder) Bar(x interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bar", reflect.TypeOf((*MockFoo)(nil).Bar), x) }自動生成されたファイルの中身を全て理解するのは難しそうですが、大体いつも同じなので、使い方さえわかっていればとりあえずよさそう。
テストファイル
モックを使う簡単な例を示します。
実際にはモックを呼び出すことが目的ではなく、あくまでモックによって外部的要因を固定化して、本当にテストしたい部分をテストする使い方になるはずですが、今回はgomockを理解するためだけなのでご理解ください。foo_test.gofunc TestFoo(t *testing.T) { // コントローラーの生成 ctrl := gomock.NewController(t) defer ctrl.Finish() // モックの生成 m := mock_foo.NewMockFoo(ctrl) // モックの対象メソッドと引数と返り値を設定する m. EXPECT(). Bar(99). Return(101) // モックの呼び出し m.Bar(99) }上記テストファイルを実行すると以下の実行結果になります。
成功結果ok study/mock (cached) Success: Tests passed.例えば、
m.Bar(100)
でモックに設定していない引数で実行すると以下のように失敗します。失敗結果--- FAIL: TestFoo (0.00s) /Users/XXX/go/src/study/mock/foo_test.go:25: Unexpected call to *mock_foo.MockFoo.Bar([100]) at /Users/XXX/go/src/study/mock/mock_foo/mock_foo.go:38 because: Expected call at /Users/XXX/go/src/study/mock/foo_test.go:22 doesn't match the argument at index 0. Got: 100 Want: is equal to 99 asm_amd64.s:522: missing call(s) to *mock_foo.MockFoo.Bar(is equal to 99) /Users/XXX/go/src/study/mock/foo_test.go:22 asm_amd64.s:522: aborting test due to missing call(s) FAIL FAIL study/mock 0.007s Error: Tests failed.参考
- 投稿日:2019-03-05T08:21:42+09:00
Go言語のDBモック(sqlmock)を分かりやすくまとめてみた
はじめに
Go言語のDBモックライブラリ(sqlmock)が体系的にまとまっている日本語ドキュメントがほとんどないようなのでまとめました。
sqlmockとは
本物のDBの代わりにSQLドライバのような振る舞いをしてくれるモックのライブラリ。
テスト時にわざわざ本物のDBを使う必要がなくなるため、「DBに入っているデータをバックアップ→テストに必要な前提データを挿入→テスト終わったらバックアップしたデータを元に戻す」みたいなことが必要なくなる。
実際にデータが挿入されたり検索されたりするわけではなく、SQLドライバの返り値にのみ注目して正常性を確認しているというところを理解するのがポイント。
ちなみに、もし実際のデータ処理の正常性まで見たいなら、テスト用DBをコンテナなりVMなりで立てて、テストデータをパターンごとにたくさん用意してテストを書くことになるでしょう。インストール
$ go get github.com/DATA-DOG/go-sqlmockいきなり具体例
公式サイトの具体例です。
この後、sqlmockの各関数などを解説します。package main import "database/sql" // テスト対象関数。関数内ではUPDATE文とINSERT文を実行している。 func recordStats(db *sql.DB, userID, productID int64) (err error) { tx, err := db.Begin() if err != nil { return } defer func() { switch err { case nil: err = tx.Commit() default: tx.Rollback() } }() if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil { return } if _, err = tx.Exec("INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)", userID, productID); err != nil { return } return }肝心のテストファイルがこちら
package main import ( "fmt" "testing" "github.com/DATA-DOG/go-sqlmock" ) // 正常系 func TestShouldUpdateStats(t *testing.T) { // DBモック用意 db, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer db.Close() mock.ExpectBegin() mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectCommit() // モック化されたDBを用いてテスト対象関数を実行 if err = recordStats(db, 2, 3); err != nil { t.Errorf("error was not expected while updating stats: %s", err) } // 使用されたモックDBが期待通りの値を持っているかを検証 if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } } // 異常系 func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) { // DBモック用意 db, mock, err := sqlmock.New() if err != nil { t.Fatalf("an error '%s' was not expected when opening a stub database connection", err) } defer db.Close() mock.ExpectBegin() mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1)) mock.ExpectExec("INSERT INTO product_viewers"). WithArgs(2, 3). WillReturnError(fmt.Errorf("some error")) mock.ExpectRollback() // モック化されたDBを用いてテスト対象関数を実行 if err = recordStats(db, 2, 3); err == nil { t.Errorf("was expecting an error, but there was none") } // 期待通りの返り値かを照合 if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) } }解説
上記の具体例で使用されているsqlmock特有の関数やメソッドについて簡単に説明します。
New
New関数で空のモックを生成する。
戻り値の1つ目(db)はモックDB接続、戻り値の2つ目(mock)はモック設定を行うために使用する。db, mock, err := sqlmock.New()ExpectBegin
ExpectBegin関数でモックの設定を開始する。
mock.ExpectBegin()ExpectExec
SQLクエリがUPDATE、INSERT、DELETE文の時はExpectExec関数を使用する。
ExpectExec関数の引数に想定するSQLクエリを指定する。
WillReturnResultメソッドの引数にSQLドライバとしての返り値をモックに設定する。
NewResult関数で引数に指定した値で返るように設定する。第一引数に主キーの自動生成ID、第二引数はSQLクエリによって影響を受けるカラムの数を指定する。mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))ExpectCommit
ExpectCommit関数でDBがコミットされるように設定する。
mock.ExpectCommit()ExpectRollback
ExpectRollback関数で、DBをコミットしようとした際にロールバックされるように設定。
mock.ExpectRollback()ExpectationsWereMet
ExpectationsWereMet関数で、モックDBが利用された後に、モックDBが期待通りに動作していたかを確認する。
if err := mock.ExpectationsWereMet(); err != nil { t.Errorf("there were unfulfilled expectations: %s", err) }上記例にないけどよく使うやつ
ExpectQuery
SQLクエリがSELECT文の時はExpectQuery関数を使用する。
ExpectQuery関数の引数に想定するSQLクエリを指定する。
WithArgs関数の引数に想定するクエリパラメータの値を指定する。
WillReturnRowsメソッドの引数にNewRows関数で作成した想定する返り値の構造体を指定する。
AddRowメソッドの引数に想定するカラムに存在する値を指定する。mock.ExpectQuery("SELECT * FROM students WHERE id = ?"). WithArgs("1"). WillReturnRows(NewRows([]string{"id", "name"}).AddRow("1", "james"))さいごに
まだあれば追記していきます。
- 投稿日:2019-03-05T08:21:17+09:00
Go言語のバリデーションチェックライブラリ(ozzo-validation)を分かりやすくまとめてみた
はじめに
APIサーバーの開発ではリクエストパラメータのバリデーションチェックは必須です。
Go言語ではバリデーションチェックのためのライブラリとして、 go-playground/validator と go-ozzo/ozzo-validationが有名なようです。
この記事では、go-ozzo/ozzo-validationについてまとめます。
go-playground/validatorに関しては、こちらの記事が分かりやすかったです。ozzo-validationとは
Go言語のバリデーション用ライブラリ。
go-playground/validatorよりは知名度が劣るものの、openapiとの親和性が良いためそこでの優位性があります。インストール
$ go get github.com/go-ozzo/ozzo-validation $ go get github.com/go-ozzo/ozzo-validation/isいきなり具体例とその解説
変数の例
stringの変数をバリデーションする最もシンプルな例です。
package main import ( "fmt" validation "github.com/go-ozzo/ozzo-validation" "github.com/go-ozzo/ozzo-validation/is" ) func main() { data := "example" err := validation.Validate(data, validation.Required, // 空を許容しない validation.Length(5, 100), // 長さが5から100まで is.URL, // URL形式のみ ) fmt.Println(err) }実行結果$ go run main.go must be a valid URL
Validate
関数でバリデーション実行する。第一引数に対象の変数を指定し、第二引数以降にルールを指定する。バリデーションを全て成功すればnilを返し、1つでも失敗すればエラーを返す。
ルールについては後述します。
上記の例では、変数dataがURL形式ではないので、バリデーションエラーとなっています。構造体の例
実際には使用頻度は構造体のバリデーションの方が高いと思います。
package main import ( "fmt" "regexp" validation "github.com/go-ozzo/ozzo-validation" ) type Address struct { Street string City string State string Zip string } func (a Address) Validate() error { return validation.ValidateStruct(&a, // Streetは空を許容せず、5から50までの長さ validation.Field(&a.Street, validation.Required, validation.Length(5, 50)), // Cityは空を許容せず、5から50までの長さ validation.Field(&a.City, validation.Required, validation.Length(5, 50)), // Stateは空を許容せず、大文字2つの文字列 validation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))), // Stateは空を許容せず、数字5つの文字列 validation.Field(&a.Zip, validation.Required, validation.Match(regexp.MustCompile("^[0-9]{5}$"))), ) } func main() { a := Address{ Street: "123", City: "Unknown", State: "Virginia", Zip: "12345", } err := a.Validate() fmt.Println(err) }実行結果$ go run main.go State: must be in a valid format; Street: the length must be between 5 and 50.
ValidateStruct
関数で構造体のバリデーションを行う。バリデーションを全て成功すればnilを返し、1つでも失敗すればエラーを返す。
Field
関数でフィールドのバリデーションを行う。呼び出す際は、第一引数に構造体のポインタを渡し、第二引数以降でルールを指定する。
ルールに関しては後述します。
もしバリデーションに失敗したフィールドがあれば、エラーは保存され、次のフィールドのバリデーションを行います。よく使うvalidatinルール
- In(...interface{}): リスト中に値があるかのチェック
validation.Field(&c.Gender, validation.In("Female", "Male")),
- Length(min, max int): strings/slices/maps/arrays 型の長さが指定範囲内かのチェック
validation.Field(&c.Name, validation.Required, validation.Length(5, 20)),
- Min(min interface{}) and Max(max interface{}): int/uint/float/time.Time型の値が指定範囲内かのチェック
validation.Field(&a.Value, validation.Min(1), validation.Max(3)),
- Match(*regexp.Regexp): 正規表現にマッチしているかのチェック
大文字2つで構成されているかどうかvalidation.Field(&a.State, validation.Required, validation.Match(regexp.MustCompile("^[A-Z]{2}$"))),
- Date(layout string): 指定日付フォーマット形式かのチェック
validation.Field(&a.ShippedAt, validation.Date("2018-01-01")),
- Required: 空でないことのチェック。nil/""/0を許容しない。
validation.Field(&a.Street, validation.Required)
- NotNil: ポインタの値がnilじゃないことのチェック。""/0は許容する。
validation.Field(&a.SampleNotNil, validation.NotNil),ここからisルール。
空の場合はエラーにならない。
注意点として、対象は全て文字列である必要がある。
- Email: Eメール形式かどうかをチェック。
validation.Field(&c.Sample, is.Email),
- URL: URL形式かどうかをチェック。
validation.Field(&c.Sample, is.URL),
- Alpha: 英字(a-zA-Z)のみで構成されているかチェック。
validation.Field(&c.Sample, is.Alpha),
- Digit: 数字(0-9)のみで構成されているかチェック。
validation.Field(&c.Sample, is.Digit),
- Alphanumeric: 英数字(a-zA-Z0-9)のみで構成されているかチェック。
validation.Field(&c.Sample, is.Alphanumeric),
- LowerCase: 小文字のみで構成されているかチェック。
validation.Field(&c.Sample, is.LowerCase),
- UpperCase: 大文字のみで構成されているかチェック。
validation.Field(&c.Sample, is.UpperCase),
- Int: int型の文字のみで構成されているかチェック。
validation.Field(&c.Sample, is.Int),
- Float: float型の文字のみで構成されているかチェック。
validation.Field(&c.Sample, is.Float),
- UUID: UUID形式かどうかをチェック。
validation.Field(&c.Sample, is.UUID),
- CreditCard: クレジットカード形式かどうかをチェック。
validation.Field(&c.Sample, is.CreditCard),
- JSON: json形式かどうかをチェック。
validation.Field(&c.Sample, is.JSON),
- MAC: MACアドレス形式かどうかをチェック。
validation.Field(&c.Sample, is.MAC),
- IP: IPアドレス形式かどうかをチェック。
validation.Field(&c.Sample, is.IP),その他
バリデーション中のinternal server error
バリデーション処理中のエラーがバリデーションルール違反なのかinternal server errorなのかを切り分けられるようにするためには、以下のように実装する。
if err := a.Validate(); err != nil { if e, ok := err.(validation.InternalError); ok { log.Println(e.InternalError()) } }参考
- 投稿日:2019-03-05T00:29:16+09:00
Goを読んでDockerの抽象構文木の構造をサクッと理解する
こんにちはpo3rinです。Dockerfile の抽象構文木(以降 AST と呼ぶ)ってどうなっているんだろうと思い調べてみました。
Dockerfile の AST を所得する
下記の Dockerfile の AST をみてみます。
FROM golang:latest WORKDIR /go ADD . /go CMD ["go", "run", "main.go"]mody の buildkit が Dockerfile の Parser を提供しているのでそれを使います。
package main import ( "fmt" "log" "os" "github.com/moby/buildkit/frontend/dockerfile/parser" ) func main() { f, _ := os.Open("./Dockerfile") r, _ := parser.Parse(f) ast := r.AST.Dump() fmt.Printf("%+v", ast) }これで文字列化された Dockerfile の AST の Dump がみれます。
$ go run main.go (from "golang:latest") (workdir "/go") (add "." "/go") (cmd "go" "run" "main.go")かなり、簡略化して表示されるので、AST の構造までは覗けません。そこはコードを読んでいく必要がありそです。
DockerfileのASTの構造を知る
github.com/moby/buildkit/frontend/dockerfile/parser を読むと下記の構造体があります。
// Node is a structure used to represent a parse tree. type Node struct { Value string // actual content Next *Node // the next item in the current sexp Children []*Node // the children of this sexp Attributes map[string]bool // special attributes for this node Original string // original line used before parsing Flags []string // only top Node should have this set StartLine int // the line in the original dockerfile where the node begins endLine int // the line in the original dockerfile where the node ends }Node は解析木を表すために使用される構文コードです。基本的に Value、 Next、 Children の3つのフィールドを使います。Value は現在のトークンの文字列値です。 Next は次のトークンで、Children はすべての子Nodeのスライスになっています。
他の言語のASTをみたことある人はかなりシンプルな構造だと感じると思いますが、実は公式も「素直に言ってかなりお粗末」だと言っています。しかし、Dockerfile はプログラミング言語よりもシンプルなので、Node もシンプルであることはむしろ効果的だと言っています。
This data structure is frankly pretty lousy for handling complex languages, but lucky for us the Dockerfile isn't very complicated. This structure. works a little more effectively than a "proper" parse tree for our needs.
さてNodeが実際にどのように構成されているのか見てみましょう。ネストした構造体を覗くときは
github.com/kr/pretty
が便利なのでこれを使いましょう。まずは大枠を掴みます。package main import ( "os" "github.com/kr/pretty" "github.com/moby/buildkit/frontend/dockerfile/parser" ) func main() { f, _ := os.Open("./Dockerfile") r, _ := parser.Parse(f) pretty.Print(r.AST) }出力はこうなります。
&parser.Node{ Value: "", Next: (*parser.Node)(nil), Children: { &parser.Node{ Value: "from", Next: &parser.Node{ Value: "golang:latest", Next: (*parser.Node)(nil), Children: nil, Attributes: {}, Original: "", Flags: nil, StartLine: 0, endLine: 0, }, Children: nil, Attributes: {}, Original: "FROM golang:latest", Flags: {}, StartLine: 1, endLine: 1, }, &parser.Node{ Value: "workdir", Next: &parser.Node{ Value: "/go", Next: (*parser.Node)(nil), Children: nil, Attributes: {}, Original: "", Flags: nil, StartLine: 0, endLine: 0, }, Children: nil, Attributes: {}, Original: "WORKDIR /go", Flags: {}, StartLine: 3, endLine: 3, }, &parser.Node{ Value: "add", Next: &parser.Node{ Value: ".", Next: &parser.Node{ Value: "/go", Next: (*parser.Node)(nil), Children: nil, Attributes: {}, Original: "", Flags: nil, StartLine: 0, endLine: 0, }, Children: nil, Attributes: {}, Original: "", Flags: nil, StartLine: 0, endLine: 0, }, Children: nil, Attributes: {}, Original: "ADD . /go", Flags: {}, StartLine: 4, endLine: 4, }, &parser.Node{ Value: "cmd", Next: &parser.Node{ Value: "go", Next: &parser.Node{ Value: "run", Next: &parser.Node{ Value: "main.go", Next: (*parser.Node)(nil), Children: nil, Attributes: {}, Original: "", Flags: nil, StartLine: 0, endLine: 0, }, Children: nil, Attributes: {}, Original: "", Flags: nil, StartLine: 0, endLine: 0, }, Children: nil, Attributes: {}, Original: "", Flags: nil, StartLine: 0, endLine: 0, }, Children: nil, Attributes: {"json":true}, Original: "CMD [\"go\", \"run\", \"main.go\"]", Flags: {}, StartLine: 6, endLine: 6, }, }, Attributes: {}, Original: "", Flags: nil, StartLine: 1, endLine: 6, }全体像は下記のようになります。
1行の中でもtokenごとにNodeに分けられます。一番上のNodeがルートNodeと呼ばれます。ルートNode自身はValueを持たずChildren Nodeの一覧を保持します。こう見るとルートNodeのChildrenの数はイメージのレイヤ数と基本的に一致します。Next Nodeは同じ行の中の次のtokenのNodeです。Flagsは
--from=builder
などのDockerfile上で使われるFlagか格納されます。StartLineとendlineは文字通り、そのノードのDockerfileにおける行数です。Originalは解析前に使用された元の行を格納しています。PrintWarnings を使って Dockerfile に対する Warning を見る
Dockerfile をビルドする際に稀に出る Warning は Dockerfile の parse 時に検知しています。mody は下記のようなparseした後のASTに対してWarningをだすメソッドもあります。
func (r *Result) PrintWarnings(out io.Writer)このような空白行があると記載がDockerfileにあると
RUN echo "Hello" && \ # (空行) echo "Docker AST"このようなDockerfileに対して PrintWarnings メソッドを使うと下記のような Warning を吐きます。
[WARNING]: Empty continuation line found in: RUN echo "Hello" && echo "Docker AST" [WARNING]: Empty continuation lines will become errors in a future release.おまけ : ASTを使ったDockerfileのLint
Dockerfile の AST が手に入ったので簡単なlintツールも作れそうです。Dockerfile におけるベストプラクティスの一つはレイヤの数を最小にすることです。つまり、二回連続でRUNを使っている Dockerfile は一つのコマンドに統合すべきです。下のコードは RUN を二連続で呼び出している部分を検知します。
package main import ( "log" "os" "github.com/kr/pretty" "github.com/moby/buildkit/frontend/dockerfile/parser" ) func main() { f, _ := os.Open("./Dockerfile") r, _ := parser.Parse(f) pretty.Print(r.AST) var valueToken string for _, child := range r.AST.Children { if valueToken == child.Value { log.Fatal("RUN is used in two consecutive layers") } valueToken = child.Value } }こんな Dockerfile は
FROM golang:latest WORKDIR /go ADD . /go RUN echo "Hello" RUN echo "Docker AST" CMD ["go", "run", "main.go"]こういう風に検知できますね。
$ go run main.go 2019/03/04 23:36:10 RUN is used in two consecutive layersDockerfile の AST はシンプルゆえ複雑なことはできませんが、これくらいの検知なら十分です。
終わりに
簡単に Dockerfile の AST を追ってみました。今後このASTを使ってどのようにビルドしているのか追ってみて、暇ならまた記事にします。