- 投稿日:2019-02-15T23:14:32+09:00
golangでGAEを始める際のメモ
どんなことでもメモしておけ、という先人の教えに従って。GAEの開発手順をメモしておく。
参考資料:
https://gist.github.com/voluntas/892570de9c38e9b3a4170c5728ba72edgolangのインストール
最近はインストール管理ツールを使う人もいるらしい。
好みの問題だと思うが、そのツールの安全性や安定性、更新頻度などに引きずられるので、個人的には直接インストールした。
https://golang.org/dl/golangの環境変数設定
golangは3つのpath設定が必要。
path
goroot
gopathgopathはインポートするパッケージを置くので容量に余裕のあるドライブを選ぶべき。
ちりも積もると意外に容量を食う。Python2.xのインストール
ローカルでのサーバエミュレートにPythonを使用する。
3.x系では動かないため、2.xの最新をインストールすること。
(GoogleCloudSDKにバンドルしてくれるというが、手元の環境ではうまくいかなかった)
https://www.python.org/downloads/windows/Python2.xの環境変数設定
GoogleCloudSDKのインストール
デフォルトだとURLに半角スペースつきでインストールされる。
コマンドラインで呼び込んだ際、半角スペースのせいで動作しない。
必ず自身でフォルダを作り、そこにインストールすること。
https://cloud.google.com/sdk/GoogleCloudSDKの初期設定
windowsでツールで入れていたら、自動で起動するオプションが出るはず。
出なかったらコマンドラインから
gcloud init
gcloud components install app-engine-gogoogle_appengineの環境変数設定
下記をpathに突っ込む。
[ドライブ]/google-cloud-sdk\platform\google_appengineサンプルを落とす
git clone -b part1-helloworld https://github.com/GoogleCloudPlatform/appengine-guestbook-go.git helloworld
cd helloworldサーバエミュレートする
goapp serve
注意
環境変数は再起動しないと読み込まないので、ちょこちょこ再起動する。
一回の再起動で何故か反応しない場合、あきらめずに再起動する。
- 投稿日:2019-02-15T21:42:23+09:00
goのchannelを使ってconnection poolっぽいものを書いてみた
ある処理を行うデーモンを何個か作って並列に実行し
あるデーモンでエラーが起きたら再起動させる処理を書いてみました。traceしやすいので
Daemon interface に自身のIDを出せるように設定しました。interfaceとgoのchannelは便利だなぁと実感できるかと思います。
package main import ( "context" "errors" "fmt" "sync" "time" ) type Daemon interface { Do(ctx context.Context) chan error ID() string } type Sample struct { interval int id string } func (a *Sample) Do(ctx context.Context) chan error { errCh := make(chan error) go func() { time.Sleep(time.Duration(a.interval) * time.Second) fmt.Printf("%#v, %#v\n", "fire", a.ID()) errCh <- errors.New("err!!!") }() return errCh } func (a *Sample) ID() string { return a.id } type ConnTable struct { list map[chan error]Daemon } func (a *ConnTable) Start(ctx context.Context, d []Daemon) chan error { errCh := make(chan error) for _, item := range d { ch := item.Do(ctx) a.list[ch] = item } go func() { for ch, daemon := range a.list { go func(noticeCh chan error, d Daemon) { for { err := <-noticeCh fmt.Printf("[%#v] receive Err %#v\n", d.ID(), err) noticeCh = d.Do(ctx) } }(ch, daemon) } }() return errCh } func main() { ss := []Daemon{ &Sample{ interval: 1, id: "1", }, &Sample{ interval: 2, id: "2", }, } ctx, cancel := context.WithCancel(context.Background()) ct := &ConnTable{ list: map[chan error]Daemon{}, } ct.Start(ctx, ss) time.Sleep(10 * time.Second) cancel() }結果
$ go run connpool/main.go "fire", "1" ["1"] receive Err &errors.errorString{s:"err!!!"} "fire", "2" "fire", "1" ["1"] receive Err &errors.errorString{s:"err!!!"} ["2"] receive Err &errors.errorString{s:"err!!!"} "fire", "1" ["1"] receive Err &errors.errorString{s:"err!!!"} "fire", "2" ["2"] receive Err &errors.errorString{s:"err!!!"} "fire", "1" ["1"] receive Err &errors.errorString{s:"err!!!"} "fire", "1" ["1"] receive Err &errors.errorString{s:"err!!!"} "fire", "2" ["2"] receive Err &errors.errorString{s:"err!!!"} "fire", "1" ["1"] receive Err &errors.errorString{s:"err!!!"} "fire", "1" ["1"] receive Err &errors.errorString{s:"err!!!"} "fire", "2" ["2"] receive Err &errors.errorString{s:"err!!!"} "fire", "1" ["1"] receive Err &errors.errorString{s:"err!!!"} "fire", "1" ["1"] receive Err &errors.errorString{s:"err!!!"}Todo
再起動時の処理をサーキットブレーカにすればもっとよくなるはず
- 投稿日:2019-02-15T21:22:55+09:00
Goの新しいerrors パッケージ xerrors(Go 1.13からは標準のerrorsパッケージに入る予定)
先日 xerrors パッケージがリリースされました。
Goの標準ライブラリではありませんが、Go公式がメンテナンスをしています。
このパッケージは、Proposal: Go 2 Error Inspection で提案されているものをGo1に実装したものです。またGo 1.13から errors packageに組み込まれる予定になっています。このパッケージができた背景は、今まで多くのGoエンジニアは下位層のエラーの情報を伝播させるために pkg/errors パッケージ などの外部ライブラリを利用してきました。この手法が開発者の間で普及したため標準ライブラリで正式に採用することになりました。
この記事ではxerrorsパッケージの仕様を紹介します。
フォーマットとラップ
文字列からのエラーを作成する
err := xerrors.New("error in main method") fmt.Printf("%v\n", err)error in main methodxerrors.Newで作成したエラーは、
%+v
のときにファイル名やメソッド名を表示します。err := xerrors.New("error in main method") fmt.Printf("%+v\n", err)error in main method: main.main /Users/sonatard/tmp/xerrors/main.go:9
%#v
では以下のようになります。err := xerrors.New("error in main method") fmt.Printf("%#v\n", err)error in main method既存のエラーから新規のエラーを作成する
baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method %v",baseErr) fmt.Printf("%+v\n", err)error in main method base error: main.main /Users/sonatard/tmp/xerrors/main.go:10このやり方だと、baseErrの行数の情報が失われてしまっています。
以下のように%v
の前にコロンとスペースを加えて: %v
とすることで既存のerrorの情報を出力することができます。baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method: %v", baseErr) fmt.Printf("%+v\n", err)error in main method: main.main /Users/sonatard/tmp/xerrors/main.go:11 - base error: main.main /Users/sonatard/tmp/xerrors/main.go:10
%v
だけではなく%s
でも問題ありません。エラーをラップする
baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method: %w", baseErr) fmt.Printf("%+v\n", err)error in main method: main.main /Users/sonatard/tmp/xerrors/main.go:11 - base error: main.main /Users/sonatard/tmp/xerrors/main.go:10
: %w
でラップできますが、%w
では正しくラップできません。baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method %w", baseErr) fmt.Printf("%+v\n", err)error in main method %!w(*xerrors.errorString): main.main /Users/sonatard/tmp/xerrors/main.go:11エラーをアンラップする
baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method: %w", baseErr) fmt.Printf("%+v\n", xerrors.Unwrap(err))base error: main.main /Users/sonatard/tmp/xerrors/main.go:10アンラップできるエラーは
: %w
でラップしたものだけであり、: %v
や: %s
ではラップされていないためアンラップできません。baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method: %v", baseErr) fmt.Printf("%+v\n", xerrors.Unwrap(err))<nil>エラーの同一性をチェックする
通常
baseErr := xerrors.New("base error") fmt.Printf("%v\n", xerrors.Is(baseErr, baseErr)) fmt.Printf("%v\n", baseErr == baseErr)true trueラップした場合
baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method: %w", baseErr) fmt.Printf("%v\n", xerrors.Is(err, baseErr)) fmt.Printf("%v\n", err == baseErr)true false
Is
メソッドを使うことで、errの中のラップされたbaseErrが同一と判断されます。複数回ラップした場合
baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method: %w", baseErr) err2 := xerrors.Errorf("error2 in main method: %w", err) fmt.Printf("%v\n", xerrors.Is(err, baseErr)) fmt.Printf("%v\n", xerrors.Is(err2, baseErr)) fmt.Printf("%v\n", xerrors.Is(err2, err))true true trueすべて
true
となります。Opaqueメソッド実行後の同一性
baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method: %w", baseErr) err2 := xerrors.Opaque(err) fmt.Printf("%v\n", xerrors.Is(err, baseErr)) fmt.Printf("%v\n", xerrors.Is(err2, baseErr)) fmt.Printf("%v\n", err) fmt.Printf("%v\n", err2) fmt.Printf("%v\n", xerrors.Unwrap(err2))true false error in main method: base error error in main method: base error <nil>
Opaque
メソッドを実行すると、同じエラーフォマットの別のエラーが返ってきます。このエラーはIsメソッドで比較するとfalse
になりますが出力結果は同じになります。またUnwrap
を実行することはできません。途中のエラーにOpaqueメソッドを適用したエラーの同一性
baseErr := xerrors.New("base error") err := xerrors.Errorf("error in main method: %w", baseErr) err2 := xerrors.Errorf("error2 in main method: %w", xerrors.Opaque(err)) err3 := xerrors.Errorf("error3 in main method: %w", err2) fmt.Printf("%v\n", xerrors.Is(err2, baseErr)) fmt.Printf("%v\n", xerrors.Is(err2, err)) fmt.Printf("%v\n", xerrors.Is(err3, err2))false false trueerrにOpaqueを適用してerr2にラップしているため、err2とerrを含んだラップは同一ではないと判断されます。
その後追加でラップしているerr3とerr2は同一と判断されます。エラーの型変換
以降の説明で登場する独自に定義した型です。
type BaseError struct { msg string } func (e *BaseError) Error() string { return e.msg }通常
var baseErr error = &BaseError{msg: "base error"} var baseErr2 *BaseError if ok := xerrors.As(baseErr, &baseErr2); !ok { fmt.Printf("As failed\n") } fmt.Printf("%v\n", baseErr2 == baseErr)true
As
メソッドの第2引数に渡した型に変換しています。ラップした型
var baseErr error = &BaseError{msg: "base error"} err := xerrors.Errorf("error in main method: %w", baseErr) var baseErr2 *BaseError if ok := xerrors.As(err, &baseErr2); !ok { fmt.Printf("As failed\n") } fmt.Printf("%v\n", baseErr2 == baseErr)true先ほどと同様ですが、ラップした型は一致しないため更に下位層のbaseErrに変換されます。
Asのターゲットの型がerror chainに存在しない場合
var baseErr error = &BaseError{msg: "base error"} err := xerrors.Errorf("error in main method: %w", baseErr) var baseErr2 *ABaseError if ok := xerrors.As(err, &baseErr2); !ok { fmt.Printf("As failed\n") } fmt.Printf("%v\n", baseErr2 == baseErr)As failed false失敗した場合は
ok
がfalse
になります。サンプル
最後にもう少し実践的なサンプルコードを紹介します。
package main import ( "fmt" "golang.org/x/xerrors" ) var ErrNotFound = &SampleError{ statusCode: 404, level: "Error", msg: "not found", } type SampleError struct { level string statusCode int msg string } func (e *SampleError) Error() string { return fmt.Sprintf("%s: code=%d, msg=%s", e.level, e.statusCode, e.msg) } func main() { err := func1() if err != nil { var sampleErr *SampleError if xerrors.As(err, &sampleErr) { switch sampleErr.level { case "Fatal": fmt.Printf("Fatal! %v\n", sampleErr) case "Error": fmt.Printf("Error! %v\n", sampleErr) case "Warning": fmt.Printf("Warning! %v\n", sampleErr) } } fmt.Printf("%+v\n", err) return } fmt.Printf("エラーなし\n") } func func1() error { err := func2() if err != nil { return xerrors.Errorf("func1 error: %w", err) } return nil } func func2() error { err := func3() if err != nil { return xerrors.Errorf("func2 error: %w", err) } return nil } func func3() error { return ErrNotFound }Error! Error: code=404, msg=not found func1 error: main.func1 /Users/sonatard/tmp/xerrors/main.go:45 - func2 error: main.func2 /Users/sonatard/tmp/xerrors/main.go:53 - Error: code=404, msg=not foundまとめ
フォーマットでエラーをラップする仕様に最初は戸惑いましたが使う上で不便はありません。(最初は
: %v
の仕様が理解できず苦労しました)
またIsやAsがラップしたerrorに対しても有効ということで、今までのようにUnwrap(Cause)から対象のエラーを取得して比較する必要がなくなりました。
Go 1.13で導入されれば標準となるので、今から使い方に慣れておきたいと思います。またフォーマットでラップする方法はフォーマットを間違ったときにコンパイルエラーにはなりません。そこでtenntennさん作の静的解析ツールwraperrfmt を使うことで間違いを発見することができます。
- 投稿日:2019-02-15T16:53:25+09:00
Golang `encoding/csv` のReaderは行全体の文字列への参照をもつ部分文字列を返すので参照リークに気をつけよう
TL;DR
encoding/csv
のReader.Read/ReadAllは一行全体から部分文字列で切り出して[]string
を返すので部分文字列が行全体文字列への参照を持っている- なのでReadの返値の一部だけをメモリに持つつもりでも行全部への参照を持ったままになってしまうので特にでかいCSVを読むときには気をつけよう
- メモリリークかと思ったらpprofで何でヒープを消費しているか調べてみるといいかも
英語版Wikipedia対応で発覚したCSV読み込みによるメモリリーク
PediaRoute というWikipediaの任意の2ページの間をリンクだけでたどっていけるかを検索するサイトを運営しています。
最近英語版Wikipediaも検索できるように改修したんですが、それによってメモリを馬鹿食いするようになって困っていました。PediaRouteではWikipediaのページの情報のうちページタイトル、リンクデータ(読み込み位置とリンクの数)を起動時にCSVファイルから読み込んでいます。
CSVデータはこんな感じです。5,false,アンパサンド,0,161,88970362,167 10,false,言語,161,292,88970529,2540 11,false,日本語,453,1252,88973069,25868 12,false,地理学,1705,278,88998937,922 14,false,EU_(曖昧さ回避),1983,27,88999859,2 ...左からページID、自動リダイレクトページかどうかの真偽値、タイトル、正リンク位置、正リンク数、逆リンク位置、逆リンク数です。
Wikpediaには2019年2月現在、日本語版で180万、英語版で1400万ページあります。1ページ1行対応のCSVファイルになっており、日本語版で105MB, 英語版で832MBあります。
やっぱり英語Wikpediaはデータでかいなあ、とか思ってそのときにはメモリ食いまくってることもほったらかしておいたんですが、そのあとしばらく経ってちまちまとメモリ削減してたところどうも計算よりメモリを消費していることに気づきました。
起動直後に強制GC
runtime.GC()
呼んで runtime.ReadMemStats でHeapAlloc見てみたところ本来使うであろうメモリよりも大きい(読み込んでるファイルサイズより消費メモリのほうが大きい)ことに気づき、さすがにこれはなんかだめだ、ということで調査をはじめました。pprofでのヒープ調査
Golangには pprof というくっそ便利なプロファイラが標準でついているので、まずはこれを使ってCSV読み込んで
runtime.GC()
した後に何にそんなにメモリ食ってるのか見ることにしました。pprofのページにある通り、
net/http/pprof
でHTTPサーバーを立てておき、go tool pprof
で見てみました。$ go tool pprof 'http://localhost:6060/debug/pprof/heap?debug=1' Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap?debug=1 Saved profile in /Users/user/pprof/pprof.alloc_objects.alloc_space.inuse_objects.inuse_space.001.pb.gz Type: inuse_space Entering interactive mode (type "help" for commands, "o" for options) (pprof) text Showing nodes accounting for 3439.74MB, 100% of 3440.89MB total Dropped 7 nodes (cum <= 17.20MB) flat flat% sum% cum cum% 1504.58MB 43.73% 43.73% 1504.58MB 43.73% encoding/csv.(*Reader).readRecord 1140.78MB 33.15% 76.88% 1708.81MB 49.66% github.com/mtgto/pediaroute-go/internal/app/web.loadLowercaseTitleToIndices 794.38MB 23.09% 100% 1730.93MB 50.30% github.com/mtgto/pediaroute-go/internal/app/core.LoadPages 0 0% 100% 1504.58MB 43.73% encoding/csv.(*Reader).Read 0 0% 100% 3439.74MB 100% github.com/mtgto/pediaroute-go/internal/app/web.Load 0 0% 100% 3440.89MB 100% main.main 0 0% 100% 3440.89MB 100% runtime.goexit 0 0% 100% 3440.89MB 100% runtime.mainやっぱり本来使うであろうメモリよりも使用量が大きいこと、そして
encoding/csv.(*Reader).readRecord
がかなりのメモリを持ったままになっていることに気づきました。なんで・・・?メモリを食い過ぎるコード
この時点で私が書いてたコードはこんな感じです。読みやすくするためエラー処理だけ省いて載せてます。完全版はGitHubへのリンクをどうぞ。
core.gotype Page struct { Id int32 Title string IsRedirect bool ForwardLinkIndex int32 ForwardLinkLength uint32 BackwardLinkIndex int32 BackwardLinkLength uint32 } // in (CSVファイル) からPage構造体のスライスを読み込んで返す func LoadPages(in string) []Page { file, _ := os.Open(in) defer file.Close() reader := csv.NewReader(file) records, err := reader.ReadAll() if err != nil { panic(err) } pages := make([]Page, 0, len(records)) for _, record := range records { pageID, _ := strconv.Atoi(record[0]) pageIsRedirect, _ := strconv.ParseBool(record[1]) forwardLinkIndex, _ := strconv.Atoi(record[3]) forwardLinkLength, _ := strconv.Atoi(record[4]) backwardLinkIndex, _ := strconv.Atoi(record[5]) backwardLinkLength, _ := strconv.Atoi(record[6]) pages = append(pages, Page{ Id: int32(pageID), Title: record[2], IsRedirect: pageIsRedirect, ForwardLinkIndex: int32(forwardLinkIndex), ForwardLinkLength: uint32(forwardLinkLength), BackwardLinkIndex: int32(backwardLinkIndex), BackwardLinkLength: uint32(backwardLinkLength), }) } return pages }1行に7列あるCSVのうち、3番目以外はstrconvで数値/真偽値に変換していたんですが、
record[2]
だけ無変換でそのままPage構造体が持つようにしていました。こんなコードでなんで
encoding/csv.(*Reader).readRecord
がヒープをもっていると言われるのかわからず、csvのReaderのコードを読んでみました。すると
readRecord
内でパース中の行全体の文字列をもつsrcの部分文字列から返値を作っていることがわかりました。reader.go// Create a single string and create slices out of it. // This pins the memory of the fields together, but allocates once. str := string(r.recordBuffer) // Convert to string once to batch allocations dst = dst[:0] if cap(dst) < len(r.fieldIndexes) { dst = make([]string, len(r.fieldIndexes)) } dst = dst[:len(r.fieldIndexes)] var preIdx int for i, idx := range r.fieldIndexes { dst[i] = str[preIdx:idx] preIdx = idx }https://github.com/golang/go/blob/go1.11.5/src/encoding/csv/reader.go#L376-L388
原因はわかったので、
encoding/csv.(*Reader).Read
の返値をディープコピーしてから持つようにすることで行全体の文字列を持ち続けてしまうことはなくなりました。実際の修正 (コミット)$ go tool pprof 'http://localhost:6060/debug/pprof/heap' Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap Saved profile in Type: inuse_space Entering interactive mode (type "help" for commands, "o" for options) (pprof) text Showing nodes accounting for 2735.02MB, 100% of 2735.02MB total flat flat% sum% cum cum% 1149.29MB 42.02% 42.02% 1566.30MB 57.27% github.com/mtgto/pediaroute-go/internal/app/web.loadLowercaseTitleToIndices 842.03MB 30.79% 72.81% 842.03MB 30.79% github.com/mtgto/pediaroute-go/internal/app/core.CopyString (inline) 743.71MB 27.19% 100% 1168.72MB 42.73% github.com/mtgto/pediaroute-go/internal/app/core.LoadPages 0 0% 100% 2735.02MB 100% github.com/mtgto/pediaroute-go/internal/app/web.Load 0 0% 100% 2735.02MB 100% main.main 0 0% 100% 2735.02MB 100% runtime.mainreaderのソースコメントには確かにひとつの文字列からスライスで切り出していることは書かれていますが、 encoding/csvのGodoc にはそのことが書かれておらず、私のようなプログラムを書くときに罠になってるっぽいなと思ったので本家にIssue投げてみたんですが、「(不要なメモリまで参照が残り続けることとのトレードオフで)速度優先でこういう実装になってる」との回答をもらいました。
バグではないとはいえフィールド数が多いCSVなんかだとメモリのムダが発生しやすくなるので、Readerに「CSVのフィールド値をスライスで切り出さない」というフラグを新たにもたせてもいいんじゃないのかなあと個人的には思ったりしました。
まとめ
encoding/csv
のReader.Read/ReadAllは一行全体から部分文字列で切り出すため返値が行全体文字列への参照を持っている。Readの返値の一部だけをメモリに持つつもりでも行全部への参照を持ったままになってしまうので特にでかいCSVを読むときには気をつけよう- メモリリークかと思ったらpprofでヒープを消費しているか調べてみるのがよい
PediaRoute は2GBメモリのVPS上で動かしてるんですが、さらにメモリを節約しないとスワップなしでは英語版Wikipediaを検索できないのでもうちょっと改修が必要そうです。
今回の話は詳しい人からしたら「数百MBのCSVはGoで読んじゃだめ」ってことかもしれないですが、Go初心者としてはpprof使っての調査をしてみたりと勉強になりました。
スライスの一部分を参照で持ってると全体が解放されないことは知ってたんですがGoの文字列も内部的にはUTF-8形式でのバイトのスライスなんですかね。csv readerの実装見るとそんな感じに見えるんですが、実はそこはちゃんとわかってません。誰か教えてください。
- 投稿日:2019-02-15T12:43:00+09:00
Go言語でポリモーフィズムを実装 ~http.HandlerFuncに学ぶ~ #golang
はじめに
- Go言語でポリモーフィズムを実装しようとするとインターフェイスを使って実装するかと思います。
- ここではもう一つ関数型を使ってのポリモーフィズムを紹介してみる。
- 正直に言いますが下記の記事を大変に参考にさせていただきました。めちゃめちゃ勉強になりました!ありがとうございます!
インタフェースの実装パターン #golangポリモーフィズム(インターフェイスを使う)
インターフェイスを用意して構造体に実装します。
インターフェイスのメソッドを実装していれば、どんな構造体も実装できます。
言葉だけではぜんぜん理解できないので、下記にサンプルコードを用意しました。やりたいことは、
call()
というメソッドに多様性を持たせてみます。
MainCaller
構造体に、MainInterface
を実装しcall()
というメソッドを実行できるようにします。call()
メソッドを持つ、Normal
構造体とChange
構造体を用意しそれぞれ振る舞いの違うcall()
を実行してみます。package main import "fmt" // call メソッドを持つインターフェイスを定義します type MainInterface interface { call(string, string) string } //------------------------------------ // インターフェイスを実装します type MainCaller struct { MainInterface } //------------------------------------ type Normal struct { } func (n *Normal)call(x, y string) (str string){ str = fmt.Sprintf("%v , %v", x, y) return } //------------------------------------ type Change struct { } func (c *Change)call(x, y string) (str string){ str = fmt.Sprintf("change %v , %v", y, x) return } //------------------------------------ func main() { var m MainCaller fmt.Println("----- Sample 1 -----") var n = Normal{} m.MainInterface = &n fmt.Println(m.call("Hello", "World")) fmt.Println() fmt.Println("----- Sample 2 -----") var c = Change{} m.MainInterface = &c fmt.Println(m.call("Hello", "World")) }実行結果
----- Sample 1 ----- Hello , World ----- Sample 2 ----- change World , Hello
Normal
もChange
もcall
メソッドを持ち、インターフェイスに定義してあるメソッド形式と同じため、MainCaller
のMainInterface
にポインタを渡すことができます。
実行結果をみるとわかるように、 同じメソッドm.call("Hello", "World")
を実行しているのに結果が変わることがわかりました。ポリモーフィズム(type Funcを使う)
ポリモーフィズムを実現するためにもう一つの方法があります。
http.HandleFunc に学ぶ
その前に
http.HadlerFunc
を見てみましょう。https://golang.org/src/net/http/server.go?#L1959
type Handler interface { ServeHTTP(ResponseWriter, *Request) } ......... // Handler that calls f. type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }やや見慣れない書き方をしてます。
Handler
インターフェイスはServeHTTP(ResponseWriter, *Request)
関数が定義されています。
HandlerFunc
はfunc(ResponseWriter, *Request)
という関数型として定義されています。
そしてそのHandlerFunc
という関数を実装しています。
そうなるとHandlerFunc
はHandler
インターフェイスの関数を満たしているので、Handler
としての振る舞いを実装していることになります。どういうことかというと、ざっくり言えば、
HandlerFunc
の関数型を満たす関数を定義しておけば、他からはServeHTTP(ResponseWriter, *Request)
として実行できる。
ってことです。言葉だけではよくわからない(俺もよくわからない)と思いますのでサンプルコードを見てみましょう。
package main import "fmt" // インターフェイス // call(string, string) string が定義されていることに注意 type MainInterface interface { call(string, string) string } type MainCaller struct { MainInterface } // 関数型の定義 type SampleFunc func(string, string) string func (s SampleFunc) call(x, y string) string { return s(x, y) } // SampleFunc を満たす関数その1 func normalFunc(x, y string) (str string){ str = fmt.Sprintf("%v , %v", x, y) return } // SampleFunc を満たす関数その2 func changeFunc(x, y string) (str string){ str = fmt.Sprintf("change %v , %v", y, x) return } func main() { var m MainCaller fmt.Println("----- Sample 1 -----") // normalFunc をSampleFunc型にキャスト var sf1 = SampleFunc(normalFunc) m.MainInterface = sf1 fmt.Println(m.call("Hello", "World")) fmt.Println("----- Sample 2 -----") // changeFunc をSampleFunc型にキャスト var sf2 = SampleFunc(changeFunc) m.MainInterface = sf2 fmt.Println(m.call("Hello", "World")) }
normalFunc
もchangeFuc
も関数として定義したにもかかわらず、func(string, string) string
型を満たしているのでSampleFunc
型にキャストでき、どちらもcall(string, string) string
関数として実行できています。
こんな感じのポリモーフィズムの実現方法もあります。おまけ
どっちがいいのか?
どっちの方法がいいのかと言われると、ケースバイケースになるのでどちらがいいなどはありません。
上記の例で挙げた
Handler
インターフェイスですが、ドキュメントをよく見るとtimeoutHandler
やredirectHandler
として実装したり、もしくは自分でHandlerFunc
を自分で定義して登録してServeHTTP
を実行することもできます。ここで「Goプログラミング実践入門 標準ライブラリでゼロからwebアプリを作る」から引用してみましょう。
この本はGo言語でWebプログラミングをするうえでとても参考になりました。P73
ハンドラ関数を使うほうがすっきりして、同じように仕事ができるのなら、ハンドラを使うのはいったいどうしてなのでしょうか。これは結局は設計の問題になります。既存のインタフェースがあるなら、あるいはハンドラとしても使える型がほしいなら、単にそのインタフェースにメソッドServeHTTPを追加すれば、URLに割り当てられるハンドラを得ることができます。またそれによって、構築するWebアプリケーションのモジュール性を高められます。設計上の方向性の問題のようです。
ちなみに
ちなみにですが、先日Qiitaに書きましたがGo言語でロガーgologger を作ってみました。
Go言語でロガー gologgerを作ってみた
その時にJSON形式のフォーマットをするときに関数型で定義してポリモーフィズムを実現しています。
https://github.com/suganoo/gologger/blob/master/gologger.go#L133// フォーマットを変更する関数 func (g *Gologger)SetOutputFormat(typeId OutputFmtType) { switch typeId { case FmtDefault: g.FormatterInterface = MarshallFunc(defaultFormat) case FmtJSON: g.FormatterInterface = MarshallFunc(jsonFormat) default: g.FormatterInterface = MarshallFunc(defaultFormat) } } // Log Format type FormatterInterface interface { marshall(*Gologger, string, string) string } type MarshallFunc func(*Gologger, string, string) string func (m MarshallFunc) marshall(g *Gologger, logLevel string, msg string) (logMsg string){ return m(g, logLevel, msg) } // デフォルトのフォーマット関数 func defaultFormat(g *Gologger, logLevel string, msg string) (logMsg string){ ..... } // JSON形式のフォーマット関数 func jsonFormat(g *Gologger, logLevel string, msg string) (logMsg string){ .... }インターフェイスを実装するように構造体を定義しようと考えましたが、少し行数が多くなること、その構造体自体に他にやらせることがないこと、から関数型で定義してしまえばいいと考えてこのようにしました。
参考にしてみてください。
- 投稿日:2019-02-15T12:41:51+09:00
Cloud Tasksのタスク追加をCloud Datastoreトランザクションに含める
はじめに
旧くよりGoogle App EngineにあったTask Queues機能が、Cloud Tasks (2019.2.15現在beta)としてサービス化され、GAEの外側から呼び出せるようになりました。
従来Task QueuesはDatastoreのトランザクションに含めることが出来て、トランザクションがcommitされた場合のみタスクの実行を行うことが可能ですが、Cloud TasksとCloud Datastoreを(クライアントライブラリなどで)併用する場合はその機能をサポートしていません。1
DatastoreへPUTした内容をSearch APIやBigQueryへミラーリングしたい場合などに重宝していた機能なので、Cloud Tasks&Cloud Datastoreでも実現することができないか考えました。ストーリー
DatastoreにPUTしたSampleエンティティをCloud Tasks経由でSearch APIに保存します。
トランザクション内(ローカルクライアント)
- Txステータス保存。(KeyはUUID)
- DatastoreにSampleエンティティPUT
- TaskQueue Add(TxステータスKeyと現在時刻を渡す)
- commit
タスクハンドラ(GAE)
- UUIDでDatastoreよりTxステータスGET
- (GETできなかったら)リトライ。一定時間経過してたらトランザクションは失敗したとみなしてタイムアウト終了
- (GETできたら)DatastoreよりSampleエンティティを取得しSearch APIに登録
実装
ローカルクライアント
まずmain関数です。
func main() { log.SetFlags(log.Lshortfile) c := context.Background() client, err := datastore.NewClient(c, projectID) if err != nil { log.Fatal(err.Error()) } sample := tasktx.Sample{ ID: uuid.Must(uuid.NewV4()).String(), Value: rand.Float64(), CreatedAt: time.Now(), } // トランザクションのタイムアウト設定 // Taskのタイムアウトよりも短くする必要がある // Cloud Tasks APIは現状30sec以上のタイムアウトを指定できない(正式仕様かは不明) c, cancel := context.WithTimeout(c, 30*time.Second) defer cancel() _, err = client.RunInTransaction(c, func(tx *datastore.Transaction) error { // Sampleモデル保存 key := datastore.NameKey("Sample", sample.ID, nil) if _, err := tx.Put(key, &sample); err != nil { return err } // Txステータスの保存 // 複数のTask Queue起動で利用可能 txStatus := &tasktx.TxStatus{ ID: uuid.Must(uuid.NewV4()).String(), CreatedAt: time.Now(), } txStatusKey := datastore.NameKey("TxStatus", txStatus.ID, nil) if _, err := tx.Put(txStatusKey, txStatus); err != nil { return err } // タスク起動 // 必須ではないがタスクはできるだけトランザクションの最後の方でまとめて起動した方がよい // 起動〜commitの間隔が長いと指数バックオフリトライの影響でタスク完了までの間隔がさらに開く可能性あり // ていうか、Cloud TasksはdelayやETA指定できないんかな(・ω・) if err := addTask(c, txStatus.ID, sample); err != nil { return err } //↓のコメントを外せばcancel&rollbackを試せる //time.Sleep(40 * time.Second) return nil }) if err != nil { log.Fatal(err.Error()) } log.Println("done") }次にTaskQueue起動処理です。
TxステータスのKeyと実行時刻をHTTP Headerに設定して渡してます。func addTask(ctx context.Context, txID string, sample tasktx.Sample) error { client, err := cloudtasks.NewClient(ctx) if err != nil { log.Println("cloudtasks NewClient failed") return err } b, err := json.Marshal(sample) if err != nil { return err } queuePath := fmt.Sprintf("projects/%s/locations/us-central1/queues/%s", projectID, queueID) req := &taskspb.CreateTaskRequest{ Parent: queuePath, Task: &taskspb.Task{ PayloadType: &taskspb.Task_AppEngineHttpRequest{ AppEngineHttpRequest: &taskspb.AppEngineHttpRequest{ HttpMethod: taskspb.HttpMethod_POST, RelativeUri: "/putdata", Body: b, Headers: map[string]string{ "X-TaskTx-ID": txID, "X-TaskTx-DispatchTime": time.Now().Format(time.RFC3339Nano), }, }, }, }, } _, err = client.CreateTask(ctx, req) if err != nil { log.Println("CreateTask failed") return err } return nil }タスクハンドラ(GAE)
Go1.11runtimeで実装しています。
func handlePutData(w http.ResponseWriter, r *http.Request) { c := appengine.NewContext(r) txID := r.Header.Get("X-TaskTx-ID") dispatchTime, err := time.Parse(time.RFC3339Nano, r.Header.Get("X-TaskTx-DispatchTime")) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } txStatusKey := datastore.NewKey(c, "TxStatus", txID, 0, nil) var txStatus tasktx.TxStatus if err = datastore.Get(c, txStatusKey, &txStatus); err == datastore.ErrNoSuchEntity { if time.Now().Sub(dispatchTime) > 60*time.Second { log.Println("timeout") return } else { log.Println("retry") // ステータス何返すべきか迷った。Lockedはどんなもんだろ http.Error(w, err.Error(), http.StatusLocked) return } } else if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } b, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } var sample tasktx.Sample if err = json.Unmarshal(b, &sample); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // get again just in case key := datastore.NewKey(c, "Sample", sample.ID, 0, nil) if err := datastore.Get(c, key, &sample); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } index, err := search.Open("Sample") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } _, err = index.Put(c, sample.ID, &sample) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } log.Println("done") w.WriteHeader(http.StatusOK) }まとめ
従来の仕組みと比べるとかなり面倒な手順になっていますが、追加できるタスク数の制限(従来5個まで)なくなるのはちょっと嬉しいメリットかもです。
ちょっとテストした感じ想定通りに動いてそうですが、まだプロダクションレベルでは採用していません。
もし何か穴があったらツッコミいただけたら超嬉しいです。
Cloud Tasksの中の人が、Transactional tasksのユースケースを集めてるという噂を聞いたので、しかるべきところにリクエストやissueなげとくといつかトランザクションが正式サポートされるかもしれません。 ↩
- 投稿日:2019-02-15T02:38:29+09:00
VSCodeでGoのModules設定
VSCodeでGoのModulesがサポートされているという情報を目にしたので設定してみました。
https://github.com/Microsoft/vscode-go/wiki/Go-modules-support-in-Visual-Studio-Code環境
- macOS 10.12.4
- Go 1.11.5
- Visual Studio Code 1.13.1
- vscode-go 0.9.2
go mod init
,go build
などでモジュールをすでに配置してある状態から開始します。Install/Update Tools
コマンドパレットから
Go: Install/Update Tools
でツール類をひととおり最新にします。
cannot find package
ツール類を最新にするだけではパッケージが見つからないので環境変数
GO111MODULE
を設定します。settings.json{ "go.toolsEnvVars": {"GO111MODULE": "on"}, }ここまでで設定完了となることもあるようです。まだエラーが出る場合は次へ
permission denied
私の環境ではこのエラーに遭遇しました。
go build golang_org/x/crypto/cryptobyte/asn1: open /usr/local/go/pkg/darwin_amd64/vendor/golang_org/x/crypto/cryptobyte/asn1.a: permission deniedファイル保存時のビルドでエラーになるようです。
go.buildOnSave
をoff
にするとエラーは消えますが今度はlintなどが効かなくなってしまうため、以下を設定します。settings.json{ "go.installDependenciesWhenBuilding": false, }既知の不具合
以上でおおよそ動くようになると思います。が、モジュールへの定義ジャンプにはまだ不具合があるようです。
https://github.com/Microsoft/vscode-go/issues/22961回目のジャンプはOK、2回目以降はジャンプできない
これは私の環境でも再現しました。参考