- 投稿日:2020-12-08T23:27:54+09:00
Go + wire と Docker で DI してみた
この記事はぷりぷりあぷりけーしょんず Advent Calendar 2020の9日目の記事です。
はじめに
今年学んだことのアウトプット第二弾です。
本記事に書く内容に関しては、結構前に実装した際のお話になります。
また、個人開発ではなくペアプロ(@MSHR-Dec さんとの共同開発)した際のものとなります。とても楽しかった開発でした?
結構月日が立ってしまった為、忘れないうちに記事に落とし込もうと思います。本記事で話さないこと
- Go のセットアップ
- Go の書き方
- Docker のセットアップ
- 今回作成したプロジェクト詳細
- 個人的ニュース
DI とは
DI (dependency injection) はよく耳にした方が多いかと思われます。
調べると「 オブジェクトの注入 」とのような直訳した意味が多く見つかります。
こちらをもう少し噛み砕いて説明すると、オブジェクト指向プログラミングにおける開発手法の1つであり、インスタンスの生成や管理を行ってくれます。
オブジェクト指向な言語のフレームワークでは結構 DI が採用されています。Go の DI ライブラリ wire
こちらの本題に入る前に、そもそもなぜ DI をしたかったのかを簡単に説明します。
本プロジェクトでは Clean architecture を採用しており(完璧な実装ではありませんが)、依存関係逆転の法則を守るために interface を用いて型を定義し、起動のタイミングでオブジェクトの注入を行いたかった。とか、各レイヤーで単体テストを書きたかったためモックと実際のロジックをいい感じに差し込みたかった。など、interface の定義が多く、起動のタイミングやテスト実行のタイミングでインスタンスの生成をいい感じにしたかったため、DI ライブラリを導入しようとなったのが経緯となります。wire とは
Google 製の DI ライブラリとなっており、2018 年 12 月に公開されました。
特別なビルドタグをつけた Go のコードをデータソースとして、コンパイルタイムでインジェクタのコードを生成してくれるみたいです。wire 取得の説明の前に、本実装での Go のバージョンは以下となります。
$ go version go version go1.13 darwin/amd64wire は以下のコマンドで取得できます。
$ go get github.com/google/wire/cmd/wireget が完了したら wire コマンドが使用できるかと思われます。
$ wire help Usage: wire <flags> <subcommand> <subcommand args> Subcommands: check print any Wire errors found commands list all command names diff output a diff between existing wire_gen.go files and what gen would generate flags describe all known top-level flags gen generate the wire_gen.go file for each package help describe subcommands and their syntax show describe all top-level provider sets Use "wire flags" for a list of top-level flags実際に使ってみる
今回は repository 周りを例に見ていきたいと思います。
まずは interface の定義からpackage repository import ( "api/src/domain/model" ) type IArticleRepository interface { Create(article *model.Article) Update(article *model.Article) FindAll() []model.Article FindOne(ID uint64) model.Article }次に、その interface を実装した struct を定義します。
ちなみに、本プロジェクトは ORM を使用しており、Gorm ライブラリを使用しています。RDBMS は MySQL を使用しています。package datastore import ( "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "github.com/thoas/go-funk" "api/src/domain/model" "api/src/domain/repository" ) type ArticleDatastore struct { db *gorm.DB } func NewArticleDatastore(d *gorm.DB) repository.IArticleRepository { return &ArticleDatastore{ db: d, } } func (a *ArticleDatastore) Create(article *model.Article) { a.db.Create(&article) } func (a *ArticleDatastore) Update(article *model.Article) { a.db.Save(&article) } func (a *ArticleDatastore) FindAll() []model.Article { var articles []model.Article a.db.Select("id").Find(&articles) results := funk.Map(articles, func(article model.Article) model.Article { a.db.First(&article).Related(&article.User, "User").Related(&article.Shop, "Shop").Related(&article.Categories, "Categories") return article }).([]model.Article) return results } func (a *ArticleDatastore) FindOne(ID uint64) model.Article { article := model.Article{ID: ID} a.db.First(&article).Related(&article.User, "User").Related(&article.Shop, "Shop").Related(&article.Categories, "Categories") return article }ここまで定義されたオブジェクトを使用する処理が以下となります。
package interactor import ( "time" "api/src/domain/model" "api/src/domain/repository" "api/src/handler/request" "api/src/usecase" ) type ArticleInteractor struct { Repository repository.IArticleRepository } func NewArticleInteractor( repository repository.IArticleRepository, ) usecase.IArticleUsecase { return &ArticleInteractor{ Repository: repository, } } func (a *ArticleInteractor) Create(ra *request.UpsertArticleRequest) uint64 { article := model.Article{ Title: ra.Title, Body: ra.Body, Status: ra.Status, UserID: ra.UserID, ShopID: ra.ShopID, CreateAt: time.Now(), } a.Repository.Create(&article) return article.ID } func (a *ArticleInteractor) Update(id uint64, ra *request.UpsertArticleRequest) { article := model.Article{ ID: id, Title: ra.Title, Body: ra.Body, Status: ra.Status, UserID: ra.UserID, ShopID: ra.ShopID, CreateAt: time.Now(), } a.Repository.Update(&article) } func (a *ArticleInteractor) GetAll() []model.Article { return a.Repository.FindAll() } func (a *ArticleInteractor) GetOne(ID uint64) model.Article { return a.Repository.FindOne(ID) }本当は↑のオブジェクトも interface が定義されているのですが、今回は1つだけをサンプルとします。
最後に DI を定義していくのですが、ソースの先頭にビルドタグをつけるのを忘れないように気をつけましょう!
また、main パッケージの層にソースを配置しないとうまくインジェクタのコードを生成(wire generate)できませんでした。(ほんとは registory パッケージとかの中に入れたかったのですが)wire.go//+build wireinject package main import ( "github.com/google/wire" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" "api/src/infrastructure/datastore" "api/src/interactor" ) func InitArticleInteractor(d *gorm.DB) *interactor.ArticleInteractor { wire.Build( datastore.NewArticleDatastore, interactor.NewArticleInteractor, ) return nil }こちらの定義だけだと、正直不安だらけでした笑
このソースでコンストラクタ引数にちゃんと必要なもの入れてくれるの?とか色々疑いまくりましたが、こちらのソースで実際の DI を generate したら以下のソースが吐き出されました。$ wire src/wire.gowire_gen.go// Code generated by Wire. DO NOT EDIT. //go:generate wire //+build !wireinject package main import ( "github.com/jinzhu/gorm" "api/src/infrastructure/datastore" "api/src/interactor" ) import ( _ "github.com/jinzhu/gorm/dialects/mysql" ) // Injectors from wire.go: func InitArticleInteractor(d *gorm.DB) *interactor.ArticleInteractor { iArticleRepository := datastore.NewArticleDatastore(d) articleInteractor := interactor.NewArticleInteractor(iArticleRepository) return articleInteractor }こちらのソースが吐き出された時は感動しました。
勝手にコンストラクタ引数を詰めてくれて、return nil
と記述していたのに、しっかり該当のインスタンスを返してくれているではありませんか!
generate してみて感動はしましたが、wire.go
の書き方には慣れが必要そうだなという印象です。(当初はなかなか慣れることができませんでした笑)あとはエントリーポイントである main.go で
InitArticleInteractor
を呼べば、wire_gen.go
で定義した方のメソッドが呼ばれるため、依存関係が解決した状態のインスタンスを使用することができます。Docker と wire で DI を自動 Generate
本プロジェクトは Docker を使用しており、コンテナのビルド・立ち上げの際に先程の wire generate ができたらいいなと思い、やってみました。
とは言っても、以下の Dockerfile を定義する感じです。こちらは @MSHR-Dec さんにたくさん助けていただきました。DockerfileFROM golang:1.13.7-alpine3.11 as build WORKDIR /api ENV GO111MODULE=on COPY . . RUN go get github.com/google/wire/cmd/wire \ && wire src/wire.go \ && cd src \ && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../main \ && cd .. FROM alpine COPY --from=build /api/main . RUN addgroup go \ && adduser -D -G go go \ && chown -R go:go ./main CMD ["./main"]あとはビルドして、立ち上げれば自動 generate してくれて API が起動します。
まとめ
アベンドカレンダー2記事目ということもあり、勢いで書き上げたため、ちょっとわかりにくい箇所が多いかと思われます。?♂️
不備などありましたらコメントどんどんください!
今回 DI ライブラリを使用してみて、慣れない箇所には時間がかかりそうでしたが、実際 generate でソースを吐き出してみると、結構な感動だったため、今後も Golang で開発をする際は、wire を用いて開発をしていこうと思います。(DI するような実装なら)おまけ
本記事には載せられませんでしたが、本プロジェクトではホットリロードとして realize ってのを使用しています。こちらも感動ものでした。
また Dockerfile を見ていただければわかるかもですが、マルチステージビルドってのも初めて使いました。
- 投稿日:2020-12-08T21:21:41+09:00
Golangでコレクションに対して手軽にMapやFindがしたい
golangでは、コレクションを扱うときには主にfor文を使うと思います。
これは「goらしい」書き方だとは思うのですが、純粋な気持ちで書いているとfor文まみれになってしまって、コードが読みづらくなってしまうことも。golangでも、他言語によくある
Map
やFind
、Fillter
などの関数があったらいいのに…。thoas/go-funkはそんな願いを叶えてくれるライブラリです。
Map
やFind
などの便利なヘルパ関数を提供していて、コレクションに対して手軽これらの操作を行うことができます。
他にもいろいろな便利な関数が提供されているので、今回は特に業務でよく使うものをいくつか紹介していきます。※ この記事はFringe81 Advent Calendar 8日目の記事です。
注意点
最初に注意点を述べますが、
go-funk
で提供てされている多くの関数はreflectを使って実装されているため、それらの関数は返り値がinterface{}
型になることに注意が必要です。
typesafeな関数が型ごとに別で提供されている場合が多いので、できるだけそちらを利用するようにしましょう。
加えて、reflectを使って実装されている関数は、typesafeな実装に比べてパフォーマンスで劣るため、繰り返しになりますが使いたい型のtypesafeな関数が提供されている場合はそちらを使用するべきです。使う
go get github.com/thoas/go-funkimport "github.com/thoas/go-funk"メソッドの紹介
以下、サンプルコードは公式ドキュメントより転載しています。
サンプルで用いるデータモデル
type Foo struct { ID int FirstName string `tag_name:"tag 1"` LastName string `tag_name:"tag 2"` Age int `tag_name:"tag 3"` } func (f Foo) TableName() string { return "foo" } f := &Foo{ ID: 1, FirstName: "Foo", LastName: "Bar", Age: 30, }funk.Contains
iterateeの中に指定された要素があればtrueを返す。
// slice of string funk.ContainsString([]string{"foo", "bar"}, "bar") // true // slice of Foo ptr funk.Contains([]*Foo{f}, f) // true funk.Contains([]*Foo{f}, nil) // false b := &Foo{ ID: 2, FirstName: "Florent", LastName: "Messa", Age: 28, } funk.Contains([]*Foo{f}, b) // false // string funk.Contains("florent", "rent") // true funk.Contains("florent", "foo") // false // even map funk.Contains(map[int]string{1: "Florent"}, 1) // truetypesafeな実装は以下。
- ContainsFloat32
- ContainsFloat64
- ContainsInt
- ContainsInt32
- ContainsInt64
- ContainsString
- ContainsUint
- ContainsUint32
- ContainsUint64funk.Difference
2つのコレクションの差を返す。
funk.Difference([]int{1, 2, 3, 4}, []int{2, 4, 6}) // []int{1, 3}, []int{6} funk.Difference([]string{"foo", "bar", "hello", "bar"}, []string{"foo", "bar"}) // []string{"hello"}, []string{}typesana実装は以下。
- Difference
- DifferenceInt
- DifferenceInt32
- DifferenceInt64
- DifferenceString
- DifferenceUInt
- DifferenceUInt32
- DifferenceUInt64funk.Filter
スライス内の、特定の条件を満たす要素をフィルタする。
条件は関数として渡す。r := funk.Filter([]int{1, 2, 3, 4}, func(x int) bool { return x%2 == 0 }) // []int{2, 4}typesafeな実装は以下。
- FilterInt
- FilterInt32
- FilterInt64
- FilterFloat32
- FilterFloat64
- FilterString
- FilterUInt
- FilterUInt32
- FilterUInt64funk.Find
スライス内に、特定の条件を満たす要素があるかどうかを返す。
条件は関数として渡す。r := funk.Find([]int{1, 2, 3, 4}, func(x int) bool { return x%2 == 0 }) // 2typesafeな実装は以下。
- FindInt
- FindInt64
- FindFloat32
- FindFloat64
- FindStringfunk.Map
map/sliceに対して特定の操作を行う。
他の型に変換することもできる(map←→slice)r := funk.Map([]int{1, 2, 3, 4}, func(x int) int { return x * 2 }) // []int{2, 4, 6, 8} r := funk.Map([]int{1, 2, 3, 4}, func(x int) string { return "Hello" }) // []string{"Hello", "Hello", "Hello", "Hello"} r = funk.Map([]int{1, 2, 3, 4}, func(x int) (int, int) { return x, x }) // map[int]int{1: 1, 2: 2, 3: 3, 4: 4} mapping := map[int]string{ 1: "Florent", 2: "Gilles", } r = funk.Map(mapping, func(k int, v string) int { return k }) // []int{1, 2} r = funk.Map(mapping, func(k int, v string) (string, string) { return fmt.Sprintf("%d", k), v }) // map[string]string{"1": "Florent", "2": "Gilles"}返り値が
interface{}
になることに注意。
型変換しないと以降の処理で利用できない。funk.Keys
Map型のキーをsliceで返す。
funk.Keys(map[string]int{"one": 1, "two": 2}) // []string{"one", "two"} (iteration order is not guaranteed)typesafeなメソッドはないので、型変換する必要がある。
funk.Uniq
sliceの値を一意にする。
funk.Uniq([]int{0, 1, 1, 2, 3, 0, 0, 12}) // []int{0, 1, 2, 3, 12}typesafeな実装は以下
- UniqInt
- UniqInt32
- UniqInt64
- UniqFloat32
- UniqFloat64
- UniqString
- UniqUInt
- UniqUInt32
- UniqUIntfunk.Subtract
2つのコレクション間の減算結果を返す。順序を保持する。
funk.Subtract([]int{0, 1, 2, 3, 4}, []int{0, 4}) // []int{1, 2, 3} funk.Subtract([]int{0, 3, 2, 3, 4}, []int{0, 4}) // []int{3, 2, 3}stringのみtypesafeな実装が提供されている。 → SubtractString
おまけ
interface{}
からの型変換goでは、実体の型が何であるかを動的にチェックする仕組みとして型アサーションが提供されています。
↓のように書くことができます。
Tは変換したい型で、第1返り値は型アサーションが成功したときの実際の値が、第2返り値は型アサーションが成功したかどうかがboolで返ります。v, ok := x.(T)もしくは、変換候補が複数ある場合は、型switch文を使って以下のようにも書けます。
switch x.(type) { case string: s := x.(string) case int: i := x.(int) }参考
- funk - GoDoc
- thoas/go-funk
- 投稿日:2020-12-08T21:15:35+09:00
LeetCodeに毎日挑戦してみた 83. Remove Duplicates from Sorted List(Python、Go)
はじめに
無料英単語サイトE-tanを運営中の@ishishowです。
プログラマとしての能力を上げるために毎日leetcodeに取り組み、自分なりの解き方を挙げていきたいと思います。
Leetcodeとは
leetcode.com
ソフトウェア開発職のコーディング面接の練習といえばこれらしいです。
合計1500問以上のコーデイング問題が投稿されていて、実際の面接でも同じ問題が出されることは多いらしいとのことです。golang入門+アルゴリズム脳の強化のためにgoとPythonで解いていこうと思います。(Pythonは弱弱だが経験あり)
19問目(問題83)
83. Remove Duplicates from Sorted List
問題内容
Given a sorted linked list, delete all duplicates such that each element appear only once.
(日本語訳)
ソートされたリンクリストを指定して、各要素が1*回*だけ表示されるように、すべての重複を削除します。
Example 1:
Input: 1->1->2 Output: 1->2Example 2:
Input: 1->1->2->3->3 Output: 1->2->3考え方
headの参照をcurにコピーします。
curをwhileループで回していき、cur.valとcur.next.valを比較して次のvalともし同じだったら次のノードを上書きします。
違う場合はそのままノードを進めていきます。
戻り値にはheadを返します。
- 解答コード
class Solution(object): def deleteDuplicates(self, head): cur = head while cur and cur.next: if cur.next.val == cur.val: cur.next = cur.next.next else: cur = cur.next return head
- Goでも書いてみます!
func deleteDuplicates(head *ListNode) *ListNode { cur := head for cur != nil && cur.Next != nil { if cur.Next.Val == cur.Val { cur.Next = cur.Next.Next } else { cur = cur.Next } } return head }
- 投稿日:2020-12-08T20:15:31+09:00
elasticが開発した公式のGo言語ElasticSearchクライアントについてまとめてみる
これはGo Advent Calendar 2020の8日目の記事です。
先日業務の中でElasticSearchを利用する機会があり、elasticがサポートしている公式のGoクライアントをその際にあまり日本語でまとまっていた情報がなかったので、これを機にまとめてみようと思います。
go-elasticsearch
https://github.com/elastic/go-elasticsearch概要
この公式ライブラリは2019年にリリースされた比較的新しいもので、Elasticの公式のクライアントとして認定され、メンテナンスされています。
https://www.elastic.co/guide/en/elasticsearch/client/index.htmlgo-elasticsearchクライアントはバージョン6系と7系がありますが、これはそれぞれElasticSearchの6系、7系に対応するものになっているので、使用するElasticSearchのバージョンに合わせて利用するライブラリのバージョンは決定してください。
使い方
Client作成
クライアント作成は2パターンあります。まずNewDefaultClientです。こちらは引数を取らないものですが、 ELASTICSEARCH_URLという環境変数にElasticSearchのエンドポイントURLを入れておくことで自動で設定してくれます。
elasticsearch.NewDefaultClient()elasticsearch.NewClient(Config)は色々とオプションを追加できるクライアントの作成方法になります。Elastic Cloudなどを利用する場合はアドレスではなく、IDでも接続することができます。この場合はELASTICSEARCH_URLに設定された環境変数は無視されます。
CACertで証明書、RetryOnStatusでリトライするStatusの定義なども盛り込むことが可能です。cert, _ := ioutil.ReadFile("path/to/ca.crt") cfg := elasticsearch.Config{ Addresses: []string{ "http://localhost:9200", "http://localhost:9201", }, Username: "foo", Password: "bar", RetryOnStatus: []int{429, 502, 503, 504}, CACert: cert, Transport: &http.Transport{ MaxIdleConnsPerHost: 10, ResponseHeaderTimeout: time.Second, DialContext: (&net.Dialer{Timeout: time.Second}).DialContext, TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS11, }, }, } elasticsearch.NewClient(cfg)Search
検索サジェストなどで用いるSearchは以下のように使用します。
var buf bytes.Buffer query := map[string]interface{}{ "query": map[string]interface{}{ "match": map[string]interface{}{ "title": "test", }, }, } if err := json.NewEncoder(&buf).Encode(query); err != nil { log.Fatalf("Error encoding query: %s", err) } // Perform the search request. res, err = es.Search( es.Search.WithContext(context.Background()), es.Search.WithIndex("test"), es.Search.WithBody(&buf), es.Search.WithTrackTotalHits(true), es.Search.WithPretty(), ) if err != nil { log.Fatalf("Error getting response: %s", err) } defer res.Body.Close() if res.IsError() { var e map[string]interface{} if err := json.NewDecoder(res.Body).Decode(&e); err != nil { log.Fatalf("Error parsing the response body: %s", err) } else { // Print the response status and error information. log.Fatalf("[%s] %s: %s", res.Status(), e["error"].(map[string]interface{})["type"], e["error"].(map[string]interface{})["reason"], ) } }少し複雑ですが、queryに実際のリクエストで投げるJsonの構造体を参考にmap[string]interface()を定義して、検索する文字列を入れます。
HTTPでリクエスト送る際のJsonBodyがこんな感じだと
{ "size": 5, "query": { "bool": { "should": [{ "match": { "word.autocomplete": { "query": "え" } } }, { "match": { "word.readingform": { "query": "え", "fuzziness": "AUTO", "operator": "and" } } }] } }, }'Goで定義するクエリはこんな感じになります。なかなか複雑ですね・・・。
query := map[string]interface{}{ "query": map[string]interface{}{ "bool": map[string]interface{}{ "should": []map[string]interface{}{ { "match": map[string]interface{}{ "word.autocomplete": map[string]interface{}{ "query": normalized, }, }, }, { "match": map[string]interface{}{ "word.readingform": map[string]interface{}{ "query": normalized, "fuzziness": "AUTO", "operator": "and", }, }, }, }, }, }, }そしてその後それをjsonにエンコードし、Searchメソッドの引数にWithBody内に入れ、Searchメソッドを叩きます。
if err := json.NewEncoder(&buf).Encode(query); err != nil { log.Fatalf("Error encoding query: %s", err) } // Perform the search request. res, err = es.Search( es.Search.WithContext(context.Background()), es.Search.WithIndex("test"), es.Search.WithBody(&buf), es.Search.WithTrackTotalHits(true), es.Search.WithPretty(), )他にも多くの引数があることが見て取れます。withSortなどを用いるとSortなども可能となっています。
Searchメソッドのレスポンスはhttp.Responseのラッパーなようになっています。また、IsError()メソットで500エラーなどの判定をすることが可能です、
Searchなどとは異なり、IndexやCreate,Updateは比較的シンプルに記載することができます。基本的にそれぞれのXXRequestという型がgo-elasticのesapiパッケージに用意されているため、そこにリクエストする値を入れてDoメソッドを叩く形になります。
ここもレスポンスはIsErrorでチェックしてあげてください。
tag := Sample{ ID: id, Name: name, } reqByte, err := json.Marshal(tag) if err != nil { return err } requestReader := bytes.NewReader(reqByte) req := esapi.CreateRequest{ Body: requestReader, Pretty: true, } res, err := req.Do(ctx, r.client) if err != nil { return xerrors.Errorf("failed to update with elastic search. %w", err) } if res.IsError() { return xerrors.Errorf("failed to update with elastic search. Not ok. %s", res.Status()) } defer res.Body.Close()様々なオプションもその型の中で定義することが可能です。試しにUpdateRequest型を見てみましょう。基本的なリクエストのボディをBodyに格納する形になりますがHeaderやPrettyなど様々なオプションの定義ができることがみて取れますね。
type UpdateRequest struct { Index string DocumentType string DocumentID string Body io.Reader Fields []string IfPrimaryTerm *int IfSeqNo *int Lang string Parent string Refresh string RetryOnConflict *int Routing string Source []string SourceExcludes []string SourceIncludes []string Timeout time.Duration Version *int VersionType string WaitForActiveShards string Pretty bool Human bool ErrorTrace bool FilterPath []string Header http.Header ctx context.Context }以上がelasticがサポートするElasticSearchのGoクライアントの紹介になりました。
少しコード量が多くなってしまう場合もありますが、公式がメンテをしてくれることもあり安心して利用のできるライブラリなので使って損はないと思います。
- 投稿日:2020-12-08T20:14:25+09:00
A Tour of Go メモ ~More types: structs, slices, and maps~
はじめに
Go の勉強のため、A Tour of Goに取り組んでいる
今回はMore types: structs, slices, and maps
章について、学んだことを記していく使えそうなスニペット
※筆者は VSCode を使用
tys
名前、フィールドの順にカーソルが移動する
type name struct { }
forr
インデックス、要素、イテレータの順にカーソルが移動する
for _, var := range var { }struct や map の初期化の際、型を補完してくれる
ページごとの補足
Pointers
ポインタを理解するのに、この記事が参考になった
ざっくりした理解ではあるが、
- オペランド(変数および値)はメモリのどこかに格納されている
- ポインタとは、オペランドを格納しているメモリのアドレス
&
オペレータは、そのオペランドのポインタを示す*
オペレータは、そのポインタの指す先の変数を示すi := 10 fmt.Println(i) // 10 fmt.Println(&i) // 0xc000012080 fmt.Println(*&i) // 10Arrays
配列とは
Go の配列は値である
配列変数は配列全体を示す(C 言語とは違う)
配列は、名前付きフィールドではなくインデックスフィールドを使用できる構造体の一種
と考えると腹落ちしやすかったコンパイラに配列の要素数をカウントさせる
// 以下の 2 つは同じ型になる a := [6]int{2, 3, 5, 7, 11, 13} b := [...]int{2, 3, 5, 7, 11, 13}初期値
初期化時に与えた引数より配列のサイズが大きい場合、ゼロ値が代入される
primes := [10]int{2, 3, 5, 7, 11, 13} fmt.Println(primes) // [2 3 5 7 11 13 0 0 0 0]Slice length and capacity
Go Slices: usage and internalsが非常に分かりやすい
スライスは、
- 配列へのポインタ
- スライスによって参照される要素の数
- 容量は、基になる配列の要素の数
で構成される、と考えると理解しやすかった
a := [6]int{2, 3, 5, 7, 11, 13} s := a[:] printSlice(s) // len=6, cap=6, [2 3 5 7 11 13] s = s[:0] // 参照される要素の数を変更 printSlice(s) // len=0, cap=6, [] s = s[:4] // 参照される要素の数を変更 printSlice(s) // len=4, cap=6, [2 3 5 7] s = s[2:] // ポインタを s[0] から s[2]に移動する == その分、参照できるlen, capが減る printSlice(s) // len=2, cap=4, [5 7] s = s[:4] // ポインタを移動した状態で、参照される要素の数を変更 printSlice(s) // len=4, cap=4, [5 7 11 13] func printSlice(s []int) { fmt.Printf("len=%d, cap=%d, %v\n", len(s), cap(s), s) }スライスの容量を越えて延ばすと、
panic: runtime error: slice bounds out of range [:5] with capacity 4
が発生するCreating a slice with make
slice は
make
関数でも定義できる
定義:func make([]T, len, cap) []T
容量(cap)を省略すると、長さ(len)と同じになる// 同じ値を定義 fmt.Println(make([]int, 5, 5)) fmt.Println(make([]int, 5)) fmt.Println([]int{0, 0, 0, 0, 0})Slices of slices
サンプル通りに書くと、
redundant type from array, slice, or map composite
と警告が出る
入れ子の場合は内部の型を省略できる// not very well board := [][]string{ []string{"_", "_", "_"}, []string{"_", "_", "_"}, []string{"_", "_", "_"}, } // well board := [][]string{ {"_", "_", "_"}, {"_", "_", "_"}, {"_", "_", "_"}, }Appending to a slice
もし、元の配列 s が、変数群を追加する際に容量が小さい場合は、より大きいサイズの配列を割り当て直す
再割当てにはコストがかかるため、予め多くを append することが分かっているなら、必要な分だけ容量を確保したほうが良いvar s []int // 20ms: 初期容量は0 s = make([]int, 0, 1000000) // 7ms: 初期容量は1000000 for i := 0; i < 1000000; i++ { s = append(s, 0) }ちなみに、時間測定の方法はこちらが参考になった
func elapsed() func() { start := time.Now() return func() { fmt.Printf("%v", time.Since(start)) } } func main() { defer elapsed()() }Exercise: Slices
パッケージインストール
手元で動かす場合は
"golang.org/x/tour/pic"
をインストール必要がある
そのまま get しても良いが、Go Module
を使ってローカルに get してみた
Ref. 新規プロジェクトの作りかたがわからない$ go mod init {PROJECT_NAME} go get golang.org/x/tourこうすると、
go.mod
が作成され、get したパッケージのバージョン情報が記述されるmodule github.com/eyuta/goTour go 1.15 require golang.org/x/tour v0.0.0-20201207214521-004403599411実装
インデックスで挿入する場合は、
func Pic(dx, dy int) [][]uint8 { exp := make([][]uint8, dx) for i := 0; i < dx; i++ { inner := make([]uint8, dy) for j := 0; j < dy; j++ { inner[j] = uint8(i * j) } exp[i] = inner } return exp }append を使う場合、
func Pic(dx, dy int) [][]uint8 { exp := make([][]uint8, 0, dx) for i := 0; i < dx; i++ { inner := make([]uint8, 0, dy) for j := 0; j < dy; j++ { inner = append(inner, uint8(i*j)) } exp = append(exp, inner) } return exp }尚、スライスの定義を
[][]uint8{}
といったような容量を 0 にして定義した場合、若干速度が遅くなる
(Appending to a slice
参照)初期容量=dx, dy: 22ms ~ 24ms 初期容量=0 : 24ms ~ 28msインデックスと append では速度の差はほとんどなかった
Exercise: Maps
make
するの忘れがちなので注意するfunc WordCount(s string) (result map[string]int) { result = make(map[string]int) for _, field := range strings.Fields(s) { elem, ok := result[field] switch ok { case true: result[field] = elem + 1 case false: result[field] = 1 } } return }Exercise: Fibonacci closure
func fibonacci() func() int { x, y := 0, 1 return func() int { result := x x, y = y, x+y return result } }
- 投稿日:2020-12-08T20:02:28+09:00
Go制約付き高速JSONエンコーダージェネレータ
前書き
9月~10月に開催されたISUCON101で利用しようと思って、ちまちま作ってたJSONのエンコーダです。
ISUCONの練習をISUCON9の予選を使って10万点が出るまで行ったんですが、最後の方はJSONエンコードが処理時間を大きく占めていて、削りたいなと思っていたのと、作ってみると面白そうだなというのでつくってみました。https://github.com/sapphi-red/json-constantiater
ちなみに予選ではそもそもJSONがボトルネックになるところまでたどり着かず、本選ではJSONではなくprotobufだったということで結局一度も使わなかったです。
制約付き
ところで制約付きっていうのをタイトルに入れてるんですが、ISUCONでしか使わないっていう想定の上で複数の前提を置いた上でつくったためです。
具体的には
- 保守性は問わない
- 生成されたコードが読める
- 一部の仕様にしか対応しない
- 実際は例外がありうる場合でも発生しないという仮定をできるようにする
- コード生成前にコードを多少書き換える必要がある2
などです。
ISUCONの予選が9/12、本選が10/3だったのに9/6につくり始めたのでジェネレータのコードの保守性とか使いやすさとかコーナーケースとかを結構削いでいます…。ベンチマーク結果
下のほうがデータサイズが大きいです。
ベンチマーク内容ConstantiateとConstantiateNonOptimizedについて
これらの違いは
実際は例外がありうる場合でも発生しないという仮定をできるようにする
この仮定を利用しているかどうかで、Constantiateでは例えばUUIDしか入らないとわかっているフィールドでは文字列のエスケープ処理をせずそのまま文字列コピーするようにしているのに対して、ConstantiateNonOptimizedではエスケープ処理がされています。
このような処理のスキップのオプションを以下の5つ用意しています。
noescape
: エスケープをしないomitnano
:time.Time
のナノ秒を出力しないsmall
: 0~999であると仮定するunsigned
: 正の数しかとらないと仮定するnonnil
: ポインタ型でnil
をとらないと仮定する実際のプロダクトとかでは使えませんが、ISUCONなら仕様が変わらないので強い仮定を置くことができるので、このようなオプションを実装しました。
仕組み
生成されたコードを見るとほとんどわかるんですが、書き込む
[]byte
を使いまわしてメモリ確保を減らして、append
だけで文字列を構築することで命令数を減らすというのが大きな方針です。このジェネレータは大きく三つの部分、1.コード生成部分、2.コード最適化部分、3.ライブラリ部分からできています。
コード生成部分
ソースコードのASTを読み込んで構造体やコメントの情報を取り出して、文字列でソースコードを生成しています。
コード最適化部分
コード生成をした後に一旦ASTで読み出して最適化をかけてます。
この最適化は複数のappend
が並んでいるときに一つのappend
にまとめるような処理をしています。res = append(res, '{') res = append(res, `"`...) // 上が下に変換されます res = append(res, `{"`...)ライブラリ部分
生成されるコードで共通の関数が実装されてます。
文字列をエスケープしてappend
する関数などがあります。
string
で扱うとコピーが多く走ってしまうので[]byte
で基本扱う(golangでは文字列がimmutableのため)- 条件分岐よりも
map
のアクセス、さらにそれよりもslice
のアクセスのほうが速いあたりの細かい処理の調整とかをしています。
終わりに
ちまちまと書いていたらもう12月になっていました…。
ベンチマーク結果だけ見るとよさそうな感じになってはいるんですが、実際に効果が出るかどうかわからないのと、おそらくgo routineの数が増えると十分に性能を発揮できない3ので、改善点はまだまだたくさんあるといった感じですね。
次回ISUCONに出るときにもしかしたらいくつか改善して使ってみるかもしれません。
http://isucon.net/archives/54704557.html 、チーム「がんもどき」で本選に出場して総合3位を取りました
↩
sliceやmapはそのままでは正常に動かず、
type UserMap map[string]User
のように型をつくる必要があります ↩今のところmutexを効率よく使う実装にしていないため。 ↩
- 投稿日:2020-12-08T13:19:56+09:00
【Go】 AWS Athenaから取得したデータをVegaでグラフ化してみた
はじめに
GoからAthenaのクエリを叩き、Athenaで取得したデータをもとにVegaでグラフを作成します。今回は、下の画像のような散布図を作成します。作業は大きく分けて、Goでの作業とVegaでの作業に分かれます。
Vegaとは?
グラフ作成ツールで、json形式のデータを読み込ませ、簡単なグラフから複雑なインタラクティブなグラフまで、様々なグラフを作ることができます。
Goでの作業
準備
.envを用意
GoからAWSにアクセスするために、AWSクレデンシャルを記載するファイルを用意します。
- まずは、.envファイルを用意します。
- .envに
AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
を記載します。スイッチロールが必要な場合はROLE_ARN
を指定します。AWS_ACCESS_KEY_ID=XXXXX AWS_SECRET_ACCESS_KEY=XXXXX ROLE_ARN=XXXXXgo mod 初期化
Goのプログラムで必要なパッケージをダウンロードするために、下記のコマンドをターミナルで実行します。この時、
go mod init hoge
のhoge
の部分には、好きな文字列を指定することができ、ここで指定した文字列は、コンパイル後の実行ファイルの名前になります。$ go mod init hoge上記のコマンドを実行後、
go.mod
というファイルができていればOK。main.go
全体像
package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "os" "strings" "log" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/credentials" "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/athena" "github.com/joho/godotenv" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) type Plot_data struct { Unixtime string `json:"unixtime"` Duration string `json:"duration"` } const ( database = "aucfan_services_log_partition" output_location = "s3://output_bucket/2020/12/07" query = ` SELECT unixtime, duration FROM aucfan_paapi_nginx_errorlog WHERE year = 2020 AND month = 6 AND day = 1 AND status_code = 429 ORDER BY unixtime; ` ) func newSession() *session.Session { awscfg := &aws.Config{ Region: aws.String("ap-northeast-1"), Credentials: credentials.NewStaticCredentialsFromCreds(credentials.Value{ AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"), SecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), }), } sess := session.Must(session.NewSession(awscfg)) if len(os.Getenv("ROLE_ARN")) == 0 { return sess } creds := stscreds.NewCredentials(sess, os.Getenv("ROLE_ARN")) config := aws.Config{Region: sess.Config.Region, Credentials: creds} sSess := session.New(&config) return sSess } //クエリ実行 func runQuery(query string) error { var ( err error r athena.ResultConfiguration ) sSess := newSession() svc := athena.New(sSess, aws.NewConfig().WithRegion("ap-northeast-1")) //クエリをセット var s athena.StartQueryExecutionInput s.SetQueryString(query) //データベースをセット var q athena.QueryExecutionContext q.SetDatabase(database) s.SetQueryExecutionContext(&q) //結果の吐き出し用のS3バケット fmt.Println(output_location) r.SetOutputLocation(output_location) s.SetResultConfiguration(&r) //クエリ実行 result, err := svc.StartQueryExecution(&s) if err != nil { return err } fmt.Println("StartQueryExecution result:") fmt.Println(result.GoString()) var qri athena.GetQueryExecutionInput qri.SetQueryExecutionId(*result.QueryExecutionId) var qrop *athena.GetQueryExecutionOutput fmt.Println("waiting...") for { qrop, err = svc.GetQueryExecution(&qri) if err != nil { return err } if *qrop.QueryExecution.Status.State == "SUCCEEDED" || *qrop.QueryExecution.Status.State == "FAILED" { break } } if *qrop.QueryExecution.Status.State == "SUCCEEDED" { err = plot(svc, result.QueryExecutionId) return err } else { fmt.Println(*qrop.QueryExecution.Status.State) } return err } // クエリ実行結果を取得 func getData(svc *athena.Athena, id *string, token *string) (*athena.GetQueryResultsOutput, *string, error) { var err error ip := &athena.GetQueryResultsInput{ QueryExecutionId: id, NextToken: token, } op, err := svc.GetQueryResults(ip) if err != nil { return nil, nil, err } return op, op.NextToken, err } // 読み込んだ結果をjsonに書き込む func plot(svc *athena.Athena, id *string) error { var ( token *string op *athena.GetQueryResultsOutput ) token = nil ioutil.WriteFile("data.json", []byte("["), os.ModePerm) f, err := os.OpenFile("data.json", os.O_APPEND|os.O_WRONLY, 0600) if err != nil { log.Print(err) return err } defer f.Close() for { //tokenを使い、全てのデータを取得する op, token, err = getData(svc, id, token) if err != nil { return err } var ( data Plot_data eol string //行末 ) //取得したデータをjsonファイルへ書き込む for i, s := range op.ResultSet.Rows { for j, t := range s.Data { if j == 0 { //Asia/Tokyoの部分を削除 data.Unixtime = strings.Replace(*t.VarCharValue, " Asia/Tokyo", "", 1) } else { data.Duration = *t.VarCharValue } } if i != 0 { if i == (len(op.ResultSet.Rows)-1) && token == nil{ eol = "]" } else { eol = "," } jsonBytes, err := json.Marshal(data) if err != nil { return err } out := new(bytes.Buffer) json.Indent(out, jsonBytes, "", " ") plot := out.String() + eol fmt.Fprint(f, "\n" + plot) } } if token == nil { break } } return err } func handler(c echo.Context) error { fmt.Println(query) err := runQuery(query) //クエリ実行 if err != nil { return err } return c.File("data.json") } func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORS()) if err := godotenv.Load(".env"); err != nil { log.Fatal(err) } e.GET("/", handler) e.Start(":1323") }解説
定数
- database
- データを取得したいAthenaのデータベースを指定します。
- output_location
- クエリの結果の吐き出し先S3バケットを指定します。
- query
- 実行したいSQLをここに記述します 。今回のような散布図を作成する場合、
SELECT x座標, y座標
になるようにSQLを作ると良いでしょう。const ( database = "aucfan_services_log_partition" output_location = "s3://output_bucket/2020/12/07" query = ` SELECT unixtime, duration FROM aucfan_paapi_nginx_errorlog WHERE year = 2020 AND month = 6 AND day = 1 AND status_code = 429 ORDER BY unixtime; ` )main
Go echoを使って、取得したAthenaのデータをJSON形式で返すAPIサーバーを立てます。
また、.envの読み込みもここでしています。godotenv.Load
の部分には作成した.envファイルまでのファイルパスを記述します。func main() { e := echo.New() e.Use(middleware.Logger()) e.Use(middleware.Recover()) e.Use(middleware.CORS()) if err := godotenv.Load(".env"); err != nil { log.Fatal(err) } e.GET("/", handler) e.Start(":1323") }handler
http://localhost:1323/ にアクセスした時にこの関数が実行されます。ここでは、Athenaでクエリを実行後、取得したデータをまとめたjsonファイルを返しています。
func handler(c echo.Context) error { fmt.Println(query) err := runQuery(query) //クエリ実行 if err != nil { return err } return c.File("data.json") }runQuery
クエリ、データベース、S3バケットを設定後、Athenaでクエリを実行します。クエリ実行後、クエリの実行が終わるまで待ちます。クエリの実行が成功(SUCCEEDED)したら、データを取得する作業に移ります。クエリの実行が失敗(FAILED)したら、処理を終了します。
func runQuery(query string) error { var ( err error r athena.ResultConfiguration ) sSess := newSession() svc := athena.New(sSess, aws.NewConfig().WithRegion("ap-northeast-1")) //クエリをセット var s athena.StartQueryExecutionInput s.SetQueryString(query) //データベースをセット var q athena.QueryExecutionContext q.SetDatabase(database) s.SetQueryExecutionContext(&q) //結果の吐き出し用のS3バケット fmt.Println(output_location) r.SetOutputLocation(output_location) s.SetResultConfiguration(&r) //クエリ実行 result, err := svc.StartQueryExecution(&s) if err != nil { return err } fmt.Println("StartQueryExecution result:") fmt.Println(result.GoString()) var qri athena.GetQueryExecutionInput qri.SetQueryExecutionId(*result.QueryExecutionId) var qrop *athena.GetQueryExecutionOutput fmt.Println("waiting...") for { qrop, err = svc.GetQueryExecution(&qri) if err != nil { return err } if *qrop.QueryExecution.Status.State == "SUCCEEDED" || *qrop.QueryExecution.Status.State == "FAILED" { break } } if *qrop.QueryExecution.Status.State == "SUCCEEDED" { err = plot(svc, result.QueryExecutionId) return err } else { fmt.Println(*qrop.QueryExecution.Status.State) } return err }newSession
AWSに接続するためのSessionを作成します。ここで、.envに記載してクレデンシャル情報を利用しています。
func newSession() *session.Session { awscfg := &aws.Config{ Region: aws.String("ap-northeast-1"), Credentials: credentials.NewStaticCredentialsFromCreds(credentials.Value{ AccessKeyID: os.Getenv("AWS_ACCESS_KEY_ID"), SecretAccessKey: os.Getenv("AWS_SECRET_ACCESS_KEY"), }), } sess := session.Must(session.NewSession(awscfg)) if len(os.Getenv("ROLE_ARN")) == 0 { return sess } creds := stscreds.NewCredentials(sess, os.Getenv("ROLE_ARN")) config := aws.Config{Region: sess.Config.Region, Credentials: creds} sSess := session.New(&config) return sSess }plot
クエリの実行結果を読み込み、jsonファイルに書き込みます。forループの中では、1行ずつ結果を読み込み、jsonファイルに書き込んでいます。
func plot(svc *athena.Athena, id *string) error { var ( token *string op *athena.GetQueryResultsOutput ) token = nil ioutil.WriteFile("data.json", []byte("["), os.ModePerm) f, err := os.OpenFile("data.json", os.O_APPEND|os.O_WRONLY, 0600) if err != nil { log.Print(err) return err } defer f.Close() for { //tokenを使い、全てのデータを取得する op, token, err = getData(svc, id, token) if err != nil { return err } var ( data Plot_data eol string //行末 ) //取得したデータをjsonファイルへ書き込む for i, s := range op.ResultSet.Rows { for j, t := range s.Data { if j == 0 { //Asia/Tokyoの部分を削除 data.Unixtime = strings.Replace(*t.VarCharValue, " Asia/Tokyo", "", 1) } else { data.Duration = *t.VarCharValue } } if i != 0 { if i == (len(op.ResultSet.Rows)-1) && token == nil{ eol = "]" } else { eol = "," } jsonBytes, err := json.Marshal(data) if err != nil { return err } out := new(bytes.Buffer) json.Indent(out, jsonBytes, "", " ") plot := out.String() + eol fmt.Fprint(f, "\n" + plot) } } if token == nil { break } } return err }getData
Athenaのクエリ実行結果を取得している部分です。実行結果は一度に最大1000件までしか取得できないため、1000件を超える場合はtokenを利用して、ページングにより次のデータを取得することができます。ループとtokenを使えば、全てのデータを取得することができます。
func getData(svc *athena.Athena, id *string, token *string) (*athena.GetQueryResultsOutput, *string, error) { var err error ip := &athena.GetQueryResultsInput{ QueryExecutionId: id, NextToken: token, } op, err := svc.GetQueryResults(ip) if err != nil { return nil, nil, err } return op, op.NextToken, err }確認
1.コンパイルします
$ go build2.プログラムを実行します
$ ./hoge3.ブラウザで http://localhost:1323/ にアクセスし、jsonデータが表示されればOK。
Vegaでの作業
- https://vega.github.io/editor/ にアクセスします。
- エディタに下記のような内容を記述すれば、右側に空っぽのグラフが作成されます。
{ "$schema": "https://vega.github.io/schema/vega/v5.json", "width": 400, "height": 400, "padding": {"left": 25, "top": 20, "right": 5, "bottom": 80}, "title": {"text": "Graph"}, "data": [ { "name": "table", "format": { "type": "json", "parse": {"unixtime": "date", "duration": "number"} }, "async": true, "url": "http://localhost:1323/" } ], "scales": [ { "name": "xscale", "type": "time", "domain": {"data": "table", "field": "unixtime"}, "range": "width", "padding": 0.05, "round": true }, { "name": "yscale", "domain": {"data": "table", "field": "duration"}, "nice": true, "range": "height" } ], "axes": [ { "orient": "bottom", "scale": "xscale", "labelAngle": 90, "labelAlign": "left", "format": "%b %d %H:%M" }, {"orient": "left", "scale": "yscale", "title": "Response Time [sec]"} ], "marks": [ { "type": "symbol", "from": {"data": "table"}, "encode": { "enter": { "size": {"value": 5}, "x": {"scale": "xscale", "field": "unixtime"}, "y": {"scale": "yscale", "field": "duration"} } } } ] }3.先ほど作成したgoのプログラムを実行します
$ ./hoge4.ブラウザのページを更新し、数秒後にグラフが表示されればOK。
- 投稿日:2020-12-08T10:23:54+09:00
行動ログを吐き出すバッチを作ったら便利だった話
「Applibot Advent Calendar 2020」 7日目の記事になります。
前日は @kaz2ngt さんの goでデータの循環参照をチェックする(gonumパッケージ グラフ入門) という記事でした!今回は、弊社のログ基盤「Dive」を使った分析要件の話になります。
Diveについて簡単に説明すると、Diveとはデータレイクという考え方を取り入れたログ基盤です。DiveのインフラはAWSを採用しているため、S3をデータレイクとして用いています。様々なプロジェクトから吐き出される行動ログはKinesisを経てこのデータレイクに流れ着きます。そしてアナリストやCS対応者などは、Redash,TableauといったBIツールを通してこのデータレイクにアクセスすることで目的の情報を得られるようになっています。弊社のログ基盤の詳細については下記記事をご覧ください。
背景
新規開発において、ログから得られるデータの集計値が正しいかどうかを保証したいという課題がありました。BIツールなどでデータを参照して集計値をビジュアライズしたところ、それらしい数値は出てきたが本当にこれが正しいデータなのかどうかが分かりません。毎度プロジェクトのサーバーアプリケーションからログを吐き出して集計の確認をすることはかなり手間がかかるため、それを解決すべくログ吐き出しバッチを作成するに至ったというものです。
何をしたか
全API網羅シナリオをかけて1ユーザーの行動ログテンプレートを作成する
幸いにもこの課題に取り組むときに全てAPIを網羅するシナリオがあったのでこの選択肢を選ぶことができました。全てのAPIが叩かれることでほとんどの行動ログを出力することができました。仮にもしそれが選択できない状況下であれば、全てのAPIを網羅するようなユーザー行動シナリオを設計してそれを実行するという選択肢を取っていたと思います。
その際に流れたログをデータレイクであるS3から抽出してローカルに落とす
S3のAWSコンソールではディレクトリを丸ごとダウンロードすることはできないかつログの種類が数十あったため、aws s3コマンドを用いたスクリプトを作成することで簡単にダウンロードできるようになります。aws s3コマンドのサブコマンドであるcpやrmはかなり危険なコマンドなので実行前に
--dryrun
オプションをつけて確認することがおすすめです。前項で得たログの集合を1ユーザーの行動ログテンプレートとしてそれを複製するバッチプログラムを作成する
ユーザーIDや行動時日付などを適切に割り振ってテンプレートからログを複製します。
このバッチプログラムの作成にgolangを用いました。s3にアップロードする処理をgoroutineを用いて並列化することによって、バッチ処理時間を短くすることができました。バッチをDockerイメージ化してAWS Batchにデプロイし、AWS Batchインスタンス上で複製した大量のログをS3にアップロードする
AWS Batchとはその名の通り、EC2インスタンス上でバッチを実行してくれるサービスです。使用したいDockerイメージと実行するコマンドを指定することで、バッチジョブを実行してくれます。これを用いることでAWSネットワーク内でログ転送が完結させることができます。
Dive集計バッチをAWS Batch上で走らせて集計データの確認をする
バッチから流した行動ログから得られる集計値と想定値が一致するかどうかを確認します。
何がよかったか
- 集計処理の速度計測ができるようになった
- バッチで吐き出すログ量を大量にすることでログ基盤アプリケーションの実行速度テストに使える
- アナリストのクエリのチェックに役立った
- Golangの知見を得ることができた
- Kotlinは多くの言語機能を持っているがgolangは比較的シンプルなイメージ
- ライブラリ周りのエコシステムがとても充実している
- 今回の場合、sirupsen/logrus, google/uuidが素敵でした!
- goroutineによる並列化で処理効率がアップすることを実感できた
まとめ
行動ログ吐き出しバッチを作成することによって、ビッグデータから得られる集計値の精度向上に役立ちそうです。
今回作ったところまででは全てのAPIを叩くようなシナリオに対して集計値が想定値と一致するかどうかのチェックができるようになりましたが、さらに精度向上を目指す場合は精度を向上させたい集計値が如実に現れるようなシナリオを考えて、そのシナリオに対する想定値を仮定し、バッチを実行してテストをするとよいのではと考えています。今後の展望として、このような様々なシナリオが存在する場合の汎用的な設計を目指しています。
- 投稿日:2020-12-08T10:07:34+09:00
[GraphQL]gqlgenを使ってアレがしたい
概要
gqlgenを使ってGraphQLサーバを実装している。
今は開発に慣れてきた部分があるが、gqlgenの導入時に調査が必要であったアレやるには?コレやるには?を少し書いてみる。しかし、どれも大体公式ドキュメントに乗っている内容なので詳細はそちらを見るのがよい
公式doc
エラーをフックしてアプリ固有の処理をしたい
例えばResolverからエラーを返す際に必ず以下のことを行いたいケースがあるとする
- Internalエラーはログ出力したい
- エラーに応じて決められたjsonフォーマットでエラーメッセージを返したい
このようなニーズに応えるためにgqlgenのhandler.Serverには
SetErrorPresenter
という関数でresolverが返したエラーをHookする仕組みがある。GraphQLでのエラーメッセージは
extensions
というjsonキーにセットする。
他のjson階層には追加できない (将来的にGraphQL側の名前と被る可能性があるため)
https://github.com/graphql/graphql-spec/releases/tag/June2018main.gofunc main() { // 一部抜粋 srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{})) srv.SetErrorPresenter(func(ctx context.Context, err error) *gqlerror.Error { return &gqlerror.Error{ Message: err.Error(), Extensions: map[string]interface{}{ // 実際には引数のerrorをerrors.As()なりで、動的にtype, codeなどをセットする "type": "Internal", "code": "1002567", }, } }) }panicを制御したい
panic発生時にアプリケーションのプロセスが終了しないように制御したいことはただある。
エラーのHookと同じくgqlgenのhandler.ServerにはSetRecoverFunc
関数が用意されていて、これを使用してpanicした際にアプリケーションのプロセスを落とさないように制御することができるmain.gofunc main() { // 一部抜粋 srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{})) srv.SetRecoverFunc(func(ctx context.Context, err interface{}) error { log.From(ctx).Error("recovered", zap.Error(fmt.Errorf("%v", err))) return &gqlerror.Error{ Message: "server error", Extensions: map[string]interface{}{ "type": "Internal", "code": "Unknown", }, } }) }カスタム型を作りたい
gqlgenには予め用意された型が用意されているがアプリケーション独自に定義した型を使用したい場合がある。
その型を指定した場合、どのような振る舞いを行うかを表現するためのインターフェースがある。それは
UnmarshalGQL
とMarshalGQL
である。
UnmarshalGQL
はリクエストされた値をstructにバインドするときに呼び出される関数で、MarshalGQL
はjsonレスポンスを返す手前の処理として呼び出される関数。例えば
UUID
という独自の型を定義し利用する場合は以下のようになるschema.graphqlscalar UUIDgqlgen.yaml# 一部抜粋 models: UUID: model: github.com/graphql-app/graph/model.UUIDuuid.gopackage model type UUID struct { string } // UnmarshalGQL implements the graphql.Unmarshaler interface func (u *UUID) UnmarshalGQL(v interface{}) error { str, ok := v.(string) if !ok { return fmt.Errorf("uuid must be string") } if _, err := uuid.Parse(str); err != nil { return fmt.Errorf("not in uuid format: %w", err) } u.string = str return nil } // MarshalGQL implements the graphql.Marshaler interface func (u UUID) MarshalGQL(w io.Writer) { _, _ = io.WriteString(w, strconv.Quote(u.string)) }.graphqlスキーマファイルを分割したい
gqlgenのexampleではschema.graphqlの中にQuery, Mutation, scalarなど宣言されているケースが多い。1つのファイルに追記する形でももちろん良いが、役割ごとにファイル分割したい場合は以下のように分割ができる。
※
gqlgen.yml
で設定した拡張子にだけ揃えればファイル名は任意で問題ないschema.graphqlschema { query: Query mutation: Mutation } type Query type Mutationuser.graphqltype User { id: ID! name: String! } # QueryやMutationはextendキーワードを使用して宣言する extend type Query { getUser: User! } extend type Mutation { addUser: User! }scalar.graphqlscalar UUID他にも
いろいろな機能がサポートされているが、使ったことがない機能が沢山あるので利用しだい追記する
(Dataloaders、Directives、Cachingとかとか使ってみたい)
- 投稿日:2020-12-08T08:47:44+09:00
DBいじるためのGoのコードをスキーマから自動生成してくれるxoってツールを使ってみた
はじめに
GoからDBをさわるためのxoってツールがある。いまいち、使い方がよくわかってなかったので、試してみた。
xo
DBをさわるときに便利なコードを、スキーマから生成してくれるらしい。
xo is a command-line tool to generate Go code based on a database schema or a custom query.
環境
このとき作った環境を使う
Dockerを使ってPostgreSQL+Goの開発環境を作ってみた - Qiita
インストール
READMEにしたがって実行した。
go get -u golang.org/x/tools/cmd/goimports go get -u github.com/xo/xoファイル生成
QuickstartにPostgreSQLのやりかた書かれてた。
models
フォルダを作る前にやったら、フォルダがないとおこられた。/go/src/work/xo# xo pgsql://root@db/db1 -o models error: output path must be a directory and already exist when not writing to a single file次は、sslが有効になってないとおこられた。PostgreSQL立ち上げるときの設定かな。disableにしといたらいいか。
/go/src/work# xo pgsql://root@db/root -o models error: pq: SSL is not enabled on the serverこれで、
models
フォルダにファイルを生成できた。xo pgsql://root@db/root?sslmode=disable -o models使ってみる
生成されたコードを使ってみる。
まず、パッケージ名は生成先のフォルダ名になる。
// Package models contains the types for schema 'public'. package modelsここを参考にさせてもらった。
DBから直接golangのモデルを生成するxoのご紹介 — そこはかとなく書くよん。パスは
$GOPATH
からのパスで指定したらいけた。ここらへん、よくわかってない。package main import ( "database/sql" "work/models" _ "github.com/lib/pq" ) func main() { db, err := sql.Open("postgres", "host=db port=5432 user=root sslmode=disable") defer db.Close() if err != nil { panic(err) } e := &models.Employee{ EmpNumber: sql.NullInt64{ Int64: 4432, Valid: true, }, } if err = e.Insert(db); err != nil { panic(err) } }実行してみる。
go run main.go行けたっぽい。
Posticoで確認
PosticoっていうGUIクライアントで確認した。
データ入ったみたい。
よかったよかった。生成ファイルの中身
構造体が作られてる。
json
のタグもある。// Employee represents a row from 'public.employee'. type Employee struct { EmpID int `json:"emp_id"` // emp_id EmpNumber sql.NullInt64 `json:"emp_number"` // emp_number // xo fields _exists, _deleted bool }
Insert()
の中身はSQLでクエリが書かれてるだけ。// Insert inserts the Employee to the database. func (e *Employee) Insert(db XODB) error { var err error // if already exist, bail if e._exists { return errors.New("insert failed: already exists") } // sql insert query, primary key provided by sequence const sqlstr = `INSERT INTO public.employee (` + `emp_number` + `) VALUES (` + `$1` + `) RETURNING emp_id` // run query XOLog(sqlstr, e.EmpNumber) err = db.QueryRow(sqlstr, e.EmpNumber).Scan(&e.EmpID) if err != nil { return err } // set existence e._exists = true return nil }おわりに
雰囲気わかった。思ったよりシンプルやった。
go mod
とかerror
とかgomock
がよくわかってないので、そっちももうちょい試してみたい。
- 投稿日:2020-12-08T08:47:44+09:00
DBいじるためのGoコードをスキーマから自動生成してくれるxoってツールを使ってみた
はじめに
GoからDBをさわるためのxoってツールがある。いまいち、使い方がよくわかってなかったので、試してみた。
xo
DBをさわるときに便利なコードを、スキーマから生成してくれるらしい。
xo is a command-line tool to generate Go code based on a database schema or a custom query.
環境
このとき作った環境を使う
Dockerを使ってPostgreSQL+Goの開発環境を作ってみた - Qiita
インストール
READMEにしたがって実行した。
go get -u golang.org/x/tools/cmd/goimports go get -u github.com/xo/xoファイル生成
QuickstartにPostgreSQLのやりかた書かれてた。
models
フォルダを作る前にやったら、フォルダがないとおこられた。/go/src/work/xo# xo pgsql://root@db/db1 -o models error: output path must be a directory and already exist when not writing to a single file次は、sslが有効になってないとおこられた。PostgreSQL立ち上げるときの設定かな。disableにしといたらいいか。
/go/src/work# xo pgsql://root@db/root -o models error: pq: SSL is not enabled on the serverこれで、
models
フォルダにファイルを生成できた。xo pgsql://root@db/root?sslmode=disable -o models使ってみる
生成されたコードを使ってみる。
まず、パッケージ名は生成先のフォルダ名になる。
// Package models contains the types for schema 'public'. package modelsここを参考にさせてもらった。
DBから直接golangのモデルを生成するxoのご紹介 — そこはかとなく書くよん。パスは
$GOPATH
からのパスで指定したらいけた。ここらへん、よくわかってない。package main import ( "database/sql" "work/models" _ "github.com/lib/pq" ) func main() { db, err := sql.Open("postgres", "host=db port=5432 user=root sslmode=disable") defer db.Close() if err != nil { panic(err) } e := &models.Employee{ EmpNumber: sql.NullInt64{ Int64: 4432, Valid: true, }, } if err = e.Insert(db); err != nil { panic(err) } }実行してみる。
go run main.go行けたっぽい。
Posticoで確認
PosticoっていうGUIクライアントで確認した。
データ入ったみたい。
よかったよかった。生成ファイルの中身
構造体が作られてる。
json
のタグもある。// Employee represents a row from 'public.employee'. type Employee struct { EmpID int `json:"emp_id"` // emp_id EmpNumber sql.NullInt64 `json:"emp_number"` // emp_number // xo fields _exists, _deleted bool }
Insert()
の中身はSQLでクエリが書かれてるだけ。// Insert inserts the Employee to the database. func (e *Employee) Insert(db XODB) error { var err error // if already exist, bail if e._exists { return errors.New("insert failed: already exists") } // sql insert query, primary key provided by sequence const sqlstr = `INSERT INTO public.employee (` + `emp_number` + `) VALUES (` + `$1` + `) RETURNING emp_id` // run query XOLog(sqlstr, e.EmpNumber) err = db.QueryRow(sqlstr, e.EmpNumber).Scan(&e.EmpID) if err != nil { return err } // set existence e._exists = true return nil }おわりに
雰囲気わかった。思ったよりシンプルやった。
go mod
とかerror
とかgomock
がよくわかってないので、そっちももうちょい試してみたい。sqlx
ってのも。