20201208のGoに関する記事は11件です。

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/amd64

wire は以下のコマンドで取得できます。

$ go get github.com/google/wire/cmd/wire

get が完了したら 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.go
wire_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 さんにたくさん助けていただきました。

Dockerfile
FROM 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 を見ていただければわかるかもですが、マルチステージビルドってのも初めて使いました。

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

Golangでコレクションに対して手軽にMapやFindがしたい

golangでは、コレクションを扱うときには主にfor文を使うと思います。
これは「goらしい」書き方だとは思うのですが、純粋な気持ちで書いているとfor文まみれになってしまって、コードが読みづらくなってしまうことも。

golangでも、他言語によくあるMapFindFillterなどの関数があったらいいのに…。

thoas/go-funkはそんな願いを叶えてくれるライブラリです。
MapFindなどの便利なヘルパ関数を提供していて、コレクションに対して手軽これらの操作を行うことができます。
他にもいろいろな便利な関数が提供されているので、今回は特に業務でよく使うものをいくつか紹介していきます。

※ この記事はFringe81 Advent Calendar 8日目の記事です。

注意点

最初に注意点を述べますが、go-funkで提供てされている多くの関数はreflectを使って実装されているため、それらの関数は返り値がinterface{}型になることに注意が必要です。
typesafeな関数が型ごとに別で提供されている場合が多いので、できるだけそちらを利用するようにしましょう。
加えて、reflectを使って実装されている関数は、typesafeな実装に比べてパフォーマンスで劣るため、繰り返しになりますが使いたい型のtypesafeな関数が提供されている場合はそちらを使用するべきです。

使う

go get github.com/thoas/go-funk
import "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) // true

typesafeな実装は以下。
- ContainsFloat32
- ContainsFloat64
- ContainsInt
- ContainsInt32
- ContainsInt64
- ContainsString
- ContainsUint
- ContainsUint32
- ContainsUint64

funk.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
- DifferenceUInt64

funk.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
- FilterUInt64

funk.Find

スライス内に、特定の条件を満たす要素があるかどうかを返す。
条件は関数として渡す。

r := funk.Find([]int{1, 2, 3, 4}, func(x int) bool {
    return x%2 == 0
}) // 2

typesafeな実装は以下。
- FindInt
- FindInt64
- FindFloat32
- FindFloat64
- FindString

funk.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
- UniqUInt

funk.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

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

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

Example 2:

Input: 1->1->2->3->3
Output: 1->2->3

考え方

  1. headの参照をcurにコピーします。

  2. curをwhileループで回していき、cur.valとcur.next.valを比較して次のvalともし同じだったら次のノードを上書きします。

  3. 違う場合はそのままノードを進めていきます。

  4. 戻り値には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
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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.html

go-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クライアントの紹介になりました。
少しコード量が多くなってしまう場合もありますが、公式がメンテをしてくれることもあり安心して利用のできるライブラリなので使って損はないと思います。

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

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 の初期化の際、型を補完してくれる

これはなかなか便利
image.png

ページごとの補足

Pointers

ポインタを理解するのに、この記事が参考になった
ざっくりした理解ではあるが、

  • オペランド(変数および値)はメモリのどこかに格納されている
  • ポインタとは、オペランドを格納しているメモリのアドレス
  • &オペレータは、そのオペランドのポインタを示す
  • *オペレータは、そのポインタの指す先の変数を示す
i := 10
fmt.Println(i) // 10
fmt.Println(&i) // 0xc000012080
fmt.Println(*&i) // 10

Arrays

配列とは

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
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go制約付き高速JSONエンコーダージェネレータ

前書き

9月~10月に開催されたISUCON101で利用しようと思って、ちまちま作ってたJSONのエンコーダです。
ISUCONの練習をISUCON9の予選を使って10万点が出るまで行ったんですが、最後の方はJSONエンコードが処理時間を大きく占めていて、削りたいなと思っていたのと、作ってみると面白そうだなというのでつくってみました。

https://github.com/sapphi-red/json-constantiater

ちなみに予選ではそもそもJSONがボトルネックになるところまでたどり着かず、本選ではJSONではなくprotobufだったということで結局一度も使わなかったです。:pensive:

制約付き

ところで制約付きっていうのをタイトルに入れてるんですが、ISUCONでしか使わないっていう想定の上で複数の前提を置いた上でつくったためです。
具体的には

  • 保守性は問わない
  • 生成されたコードが読める
  • 一部の仕様にしか対応しない
  • 実際は例外がありうる場合でも発生しないという仮定をできるようにする
  • コード生成前にコードを多少書き換える必要がある2

などです。
ISUCONの予選が9/12、本選が10/3だったのに9/6につくり始めたのでジェネレータのコードの保守性とか使いやすさとかコーナーケースとかを結構削いでいます…。

ベンチマーク結果

image.png
image.png
image.png

下のほうがデータサイズが大きいです。
ベンチマーク内容

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に出るときにもしかしたらいくつか改善して使ってみるかもしれません。


  1. http://isucon.net/archives/54704557.html 、チーム「がんもどき」で本選に出場して総合3位を取りました :tada: 

  2. sliceやmapはそのままでは正常に動かず、type UserMap map[string]Userのように型をつくる必要があります 

  3. 今のところmutexを効率よく使う実装にしていないため。 

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

【Go】 AWS Athenaから取得したデータをVegaでグラフ化してみた

はじめに

GoからAthenaのクエリを叩き、Athenaで取得したデータをもとにVegaでグラフを作成します。今回は、下の画像のような散布図を作成します。作業は大きく分けて、Goでの作業とVegaでの作業に分かれます。

visualization.png

Vegaとは?

グラフ作成ツールで、json形式のデータを読み込ませ、簡単なグラフから複雑なインタラクティブなグラフまで、様々なグラフを作ることができます。

Goでの作業

準備

.envを用意

GoからAWSにアクセスするために、AWSクレデンシャルを記載するファイルを用意します。

  1. まずは、.envファイルを用意します。
  2. .envにAWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYを記載します。スイッチロールが必要な場合はROLE_ARNを指定します。
AWS_ACCESS_KEY_ID=XXXXX
AWS_SECRET_ACCESS_KEY=XXXXX
ROLE_ARN=XXXXX

go mod 初期化

Goのプログラムで必要なパッケージをダウンロードするために、下記のコマンドをターミナルで実行します。この時、go mod init hogehogeの部分には、好きな文字列を指定することができ、ここで指定した文字列は、コンパイル後の実行ファイルの名前になります。

$ 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 build

2.プログラムを実行します

$ ./hoge

3.ブラウザで http://localhost:1323/ にアクセスし、jsonデータが表示されればOK。
スクリーンショット 2020-12-08 12.52.50.png

Vegaでの作業

  1. https://vega.github.io/editor/ にアクセスします。
  2. エディタに下記のような内容を記述すれば、右側に空っぽのグラフが作成されます。
{
  "$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"}
        }
      }
    }
  ]
}

スクリーンショット 2020-12-08 13.08.55.png

3.先ほど作成したgoのプログラムを実行します

$ ./hoge

4.ブラウザのページを更新し、数秒後にグラフが表示されればOK。

スクリーンショット 2020-12-08 13.17.39.png

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

行動ログを吐き出すバッチを作ったら便利だった話

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を叩くようなシナリオに対して集計値が想定値と一致するかどうかのチェックができるようになりましたが、さらに精度向上を目指す場合は精度を向上させたい集計値が如実に現れるようなシナリオを考えて、そのシナリオに対する想定値を仮定し、バッチを実行してテストをするとよいのではと考えています。今後の展望として、このような様々なシナリオが存在する場合の汎用的な設計を目指しています。

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

[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/June2018

main.go
func 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.go
func 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には予め用意された型が用意されているがアプリケーション独自に定義した型を使用したい場合がある。
その型を指定した場合、どのような振る舞いを行うかを表現するためのインターフェースがある。

それはUnmarshalGQLMarshalGQLである。
UnmarshalGQLはリクエストされた値をstructにバインドするときに呼び出される関数で、MarshalGQLはjsonレスポンスを返す手前の処理として呼び出される関数。

例えばUUIDという独自の型を定義し利用する場合は以下のようになる

schema.graphql
scalar UUID
gqlgen.yaml
# 一部抜粋
models:
  UUID:
    model: github.com/graphql-app/graph/model.UUID
uuid.go
package 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.graphql
schema {
    query: Query
    mutation: Mutation
}

type Query
type Mutation
user.graphql
type User {
    id: ID!
    name: String!
}

# QueryやMutationはextendキーワードを使用して宣言する
extend type Query {
    getUser: User!
}

extend type Mutation {
    addUser: User!
}
scalar.graphql
scalar UUID

他にも

いろいろな機能がサポートされているが、使ったことがない機能が沢山あるので利用しだい追記する
(Dataloaders、Directives、Cachingとかとか使ってみたい)

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

DBいじるためのGoのコードをスキーマから自動生成してくれるxoってツールを使ってみた

はじめに

GoからDBをさわるためのxoってツールがある。いまいち、使い方がよくわかってなかったので、試してみた。

xo

DBをさわるときに便利なコードを、スキーマから生成してくれるらしい。

xo/xo: Command line tool to generate idiomatic Go code for SQL databases supporting PostgreSQL, MySQL, SQLite, Oracle, and Microsoft SQL Server

xo is a command-line tool to generate Go code based on a database schema or a custom query.

環境

このとき作った環境を使う

Dockerを使ってPostgreSQL+Goの開発環境を作ってみた - Qiita

インストール

READMEにしたがって実行した。

xo/xo: Command line tool to generate idiomatic Go code for SQL databases supporting PostgreSQL, MySQL, SQLite, Oracle, and Microsoft SQL Server

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

フォルダ構成は、こんな感じ。
image.png

使ってみる

生成されたコードを使ってみる。

まず、パッケージ名は生成先のフォルダ名になる。

// 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クライアントで確認した。
データ入ったみたい。
よかったよかった。

image.png

生成ファイルの中身

構造体が作られてる。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がよくわかってないので、そっちももうちょい試してみたい。

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

DBいじるためのGoコードをスキーマから自動生成してくれるxoってツールを使ってみた

はじめに

GoからDBをさわるためのxoってツールがある。いまいち、使い方がよくわかってなかったので、試してみた。

xo

DBをさわるときに便利なコードを、スキーマから生成してくれるらしい。

xo/xo: Command line tool to generate idiomatic Go code for SQL databases supporting PostgreSQL, MySQL, SQLite, Oracle, and Microsoft SQL Server

xo is a command-line tool to generate Go code based on a database schema or a custom query.

環境

このとき作った環境を使う

Dockerを使ってPostgreSQL+Goの開発環境を作ってみた - Qiita

インストール

READMEにしたがって実行した。

xo/xo: Command line tool to generate idiomatic Go code for SQL databases supporting PostgreSQL, MySQL, SQLite, Oracle, and Microsoft SQL Server

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

フォルダ構成は、こんな感じ。
image.png

使ってみる

生成されたコードを使ってみる。

まず、パッケージ名は生成先のフォルダ名になる。

// 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クライアントで確認した。
データ入ったみたい。
よかったよかった。

image.png

生成ファイルの中身

構造体が作られてる。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ってのも。

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