20190215のGoに関する記事は7件です。

golangでGAEを始める際のメモ

どんなことでもメモしておけ、という先人の教えに従って。GAEの開発手順をメモしておく。

参考資料:
https://gist.github.com/voluntas/892570de9c38e9b3a4170c5728ba72ed

golangのインストール

最近はインストール管理ツールを使う人もいるらしい。
好みの問題だと思うが、そのツールの安全性や安定性、更新頻度などに引きずられるので、個人的には直接インストールした。
https://golang.org/dl/

golangの環境変数設定

golangは3つのpath設定が必要。
path
goroot
gopath

gopathはインポートするパッケージを置くので容量に余裕のあるドライブを選ぶべき。
ちりも積もると意外に容量を食う。

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-go

google_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

注意

環境変数は再起動しないと読み込まないので、ちょこちょこ再起動する。
一回の再起動で何故か反応しない場合、あきらめずに再起動する。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

goのchannelを使ってconnection poolっぽいものを書いてみた

ある処理を行うデーモンを何個か作って並列に実行し
あるデーモンでエラーが起きたら再起動させる処理を書いてみました。

traceしやすいので
Daemon interface に自身のIDを出せるように設定しました。

interfaceとgoのchannelは便利だなぁと実感できるかと思います。

playground

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
再起動時の処理をサーキットブレーカにすればもっとよくなるはず

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 method

xerrors.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
true

errに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

失敗した場合は okfalse になります。

サンプル

最後にもう少し実践的なサンプルコードを紹介します。

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 を使うことで間違いを発見することができます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.go
type 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.main

readerのソースコメントには確かにひとつの文字列からスライスで切り出していることは書かれていますが、 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の実装見るとそんな感じに見えるんですが、実はそこはちゃんとわかってません。誰か教えてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

NormalChangecall メソッドを持ち、インターフェイスに定義してあるメソッド形式と同じため、 MainCallerMainInterface にポインタを渡すことができます。
実行結果をみるとわかるように、 同じメソッド 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) 関数が定義されています。
HandlerFuncfunc(ResponseWriter, *Request) という関数型として定義されています。
そしてそのHandlerFunc という関数を実装しています。
そうなるとHandlerFuncHandler インターフェイスの関数を満たしているので、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"))
}

normalFuncchangeFuc も関数として定義したにもかかわらず、func(string, string) string 型を満たしているのでSampleFunc 型にキャストでき、どちらもcall(string, string) string 関数として実行できています。
こんな感じのポリモーフィズムの実現方法もあります。

おまけ

どっちがいいのか?

どっちの方法がいいのかと言われると、ケースバイケースになるのでどちらがいいなどはありません。

上記の例で挙げたHandler インターフェイスですが、ドキュメントをよく見るとtimeoutHandlerredirectHandler として実装したり、もしくは自分で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){
....
}

インターフェイスを実装するように構造体を定義しようと考えましたが、少し行数が多くなること、その構造体自体に他にやらせることがないこと、から関数型で定義してしまえばいいと考えてこのようにしました。

参考にしてみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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に保存します。

トランザクション内(ローカルクライアント)

  1. Txステータス保存。(KeyはUUID)
  2. DatastoreにSampleエンティティPUT
  3. TaskQueue Add(TxステータスKeyと現在時刻を渡す)
  4. commit

タスクハンドラ(GAE)

  1. UUIDでDatastoreよりTxステータスGET
  2. (GETできなかったら)リトライ。一定時間経過してたらトランザクションは失敗したとみなしてタイムアウト終了
  3. (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個まで)なくなるのはちょっと嬉しいメリットかもです。

ちょっとテストした感じ想定通りに動いてそうですが、まだプロダクションレベルでは採用していません。
もし何か穴があったらツッコミいただけたら超嬉しいです。


  1. Cloud Tasksの中の人が、Transactional tasksのユースケースを集めてるという噂を聞いたので、しかるべきところにリクエストやissueなげとくといつかトランザクションが正式サポートされるかもしれません。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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でツール類をひととおり最新にします。
update_tools.png

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.buildOnSaveoffにするとエラーは消えますが今度はlintなどが効かなくなってしまうため、以下を設定します。

settings.json
{
    "go.installDependenciesWhenBuilding": false,
}

既知の不具合

以上でおおよそ動くようになると思います。が、モジュールへの定義ジャンプにはまだ不具合があるようです。
https://github.com/Microsoft/vscode-go/issues/2296

1回目のジャンプはOK、2回目以降はジャンプできない
これは私の環境でも再現しました。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む