20200228のGoに関する記事は6件です。

GoのechoでRESTAPIを作る時にCORSで怒られた時の対処法

フロントエンドとバックエンドでドメインが違う場合、フロントエンドからバックエンドにfetchするとCORSで怒られる。
その際は以下のようにCORSWithConfigミドルウェアを設定してやればいい。

server.go
e := echo.New()
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
    AllowOrigins: []string{"http:///*リクエスト送る側のurl*/"},
    AllowHeaders:     []string{"authorization", "Content-Type", "Access-Control- 
    Allow-Origin"},
    AllowCredentials: true,
    AllowMethods: []string{http.MethodGet, http.MethodPost},
}))

e.POST("/login", login)
e.GET("/logout", logout)

e.Logger.Fatal(e.Start(":/*ポート番号*/"))

func login(c echo.Context) error {
    /*handle login*/
}

func  logout(c echo.Context) error {
    /*handle logout*/
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Prometheus+Grafanaでk8s上のGoアプリケーションのメトリクスを回収して可視化

Prometheus+Grafanaで、k8s上で動かしているGoアプリケーションのMemoryやゴルーチン起動数などのメトリクスを回収し可視化する

最終的なグラフ

構成図

image.png

この記事では、Prometheus, Grafanaもk8s内で動作させます。
メトリクスの回収としては、GoのアプリケーションにPrometheus Exporterを入れてメトリクスを公開し、Promehteusはサービスディスカバリを使って、そのメトリクスを回収という挙動になります。

構築

kubernetesクラスタを立ち上げる

今回はEKSを利用します。eksctlを使うと簡単にEKSクラスタを立ち上げれます。

eksctl create cluster --name sample-cluster \
  --version 1.14 \
  --nodegroup-name sample-cluster-worker \
  --node-type t3.small \
  --nodes 3 \
  --nodes-min 1 \
  --nodes-max 5 \
  --managed

Goアプリケーションにメトリクス回収用の設定を入れる

ここでの説明で使うコードはこちらのリポジトリにおいてます


github.com/prometheus/client_golangを利用してメトリクスを公開します

go get github.com/prometheus/client_golang

あとはコードに下記のようにメトリクス公開用のポートをあげれば、アプリ側の設定はokです。

import (
    "net/http"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

func main() {
    http.Handle("/metrics", promhttp.Handler())
    http.ListenAndServe(":2112", nil)
}

ちゃんと運用する場合は下記のように別スレッドで動かしてあげると良いでしょう

func main() {
  // 設定とかで、メトリクス公開設定とか入れとく
    if conf.COLLECT_METRICS {
        http.Handle("/metrics", promhttp.Handler())
        go http.ListenAndServe(":2112", nil)
    }
  // メインの処理
  // ...
}

一度ローカルで動作確認しておきましょう, ちゃんと2112ポートでメトリクスを回収できるか確認します

$ go run main.go
$ curl localhost:2112/metrics
# HELP go_gc_duration_seconds A summary of the GC invocation durations.
# TYPE go_gc_duration_seconds summary
go_gc_duration_seconds{quantile="0"} 0
go_gc_duration_seconds{quantile="0.25"} 0
go_gc_duration_seconds{quantile="0.5"} 0
go_gc_duration_seconds{quantile="0.75"} 0
go_gc_duration_seconds{quantile="1"} 0
go_gc_duration_seconds_sum 0
go_gc_duration_seconds_count 0
...

取れていますね。あとはこれをkubernetesにdeployします。
マニフェストで2112ポートを開けておいてください

deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: sample-go-app
spec:
  selector:
    matchLabels:
      app: prometheus-export-sample
  replicas: 4
  template:
    metadata:
      labels:
        app: prometheus-export-sample
    spec:
      containers:
        - name: prometheus-export-sample
          image: esaka/prometheus-export-sample
          ports:
            - containerPort: 2112

Prometheus+Grafanaをデプロイ

こちらのマニフェストをベースにして、いくつか設定を変えたものが以下になります。

https://github.com/esakat/prometheus-exporter-sample/blob/master/prometheus-grafana.yml

変更点

  • Dashboardのimport設定削除
  • GrafanaやPrometheusのimageを新しいものに変更
  • GrafanaのサービスをLoadBalnacerに変更
  • prometheus.ymlにGoのメトリクス回収設定を追加(後述)

Goのメトリクス回収用設定を追加する

prometheus.ymlに以下のような, Goアプリケーション回収用の設定を追加します。

prometheus.yml
      - job_name: 'go-metrics'
        kubernetes_sd_configs:
        - role: pod
        relabel_configs:
        - source_labels:
          - __meta_kubernetes_namespace
          - __meta_kubernetes_pod_container_port_number
          regex: default;2112
          action: keep
        - source_labels:
          - __meta_kubernetes_pod_name
          target_label: job
        - source_labels:
          - __meta_kubernetes_pod_name
          target_label: pod

Grafanaにログイン

以下のコマンドで作成されたELBのアドレスを確認してください
立ち上がるまでは少し時間かかります。

$ kubectl get svc -n monitoring grafana 
NAME      TYPE           CLUSTER-IP       EXTERNAL-IP                                                                    PORT(S)          AGE
grafana   LoadBalancer   10.100.191.149   aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.ap-northeast-1.elb.amazonaws.com   3000:32017/TCP   7m46s

立ち上がったら、3000ポート指定でGrafanaへ繋がります。
http://aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.ap-northeast-1.elb.amazonaws.com:3000

ログインユーザーとパスワードはadmin:adminです

Prometheusをデータソースとして設定

データソース設定追加でPrometheusを選択し
URLにhttp://prometheus:9090を設定しSave & Testでおkです。

image.png

Dashboardを作成

Grafanaのサイトに行くと、色々ダッシュボードが転がっています。
特にこだわりなければ、この中からGoアプリケーション可視化用のダッシュボードを使うと楽でしょう

https://grafana.com/grafana/dashboards/6671

こちらのダッシュボードがおすすめです。
Download Jsonでjsonファイルを落としておきます。

そしたらhttp://<host>:3000/dashboard/importにアクセスしUpload .json fileボタンを押して、先ほど落としたgo-processes_rev2.jsonをアップロードします。
そしたら、prometheus-aplPrometheusを選択してImportをすれば、以下のようなダッシュボードが見れるようになります

image.png

先ほどデプロイしたsample-go-appのメトリクスが取れています。

まとめ

GoアプリにPrometheus Exporterを仕込んで、Prometheusにメトリクスを回収させ
Grafanaで可視化させました

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

golangのmongo-go-driverを使うとstructのtime.TimeにUTCで日付が入るのでlocal-timeにする方法

ctx, _ := context.WithTimeout(context.Background(), 10*time.Second)
opt := options.Client().ApplyURI("mongodb://localhost:27017")

// findでstructにtime.Time型が設定されている状態で、取得するとUTCで設定されるのでLocalTimeZoneになるようにした。
rb := bson.NewRegistryBuilder()
// v1.2.1より前
// rb.RegisterDecoder(reflect.TypeOf(time.Time{}), bsoncodec.NewTimeCodec(bsonoptions.TimeCodec().SetUseLocalTimeZone(true)))
// v1.3.0以降 メソッドが変更になっている。。。
rb.RegisterTypeDecoder(reflect.TypeOf(time.Time{}), bsoncodec.NewTimeCodec(bsonoptions.TimeCodec().SetUseLocalTimeZone(true)))
opt.SetRegistry(rb.Build())

client, _ := mongo.Connect(ctx, opt)
col := client.Database("hoge").Collection("fuga")

type fuga struct {
T1 time.Time          `bson:"t1"`
T2 primitive.DateTime `bson:"t2"`
}
now := time.Now()
col.DeleteMany(ctx, &map[string]interface{}{})
col.InsertOne(ctx, &map[string]interface{}{"t1": now, "t2": now})

result := &fuga{}

sr := col.FindOne(ctx, &map[string]interface{}{})
sr.Decode(result)

fmt.Println(result.T1)
fmt.Println(result.T2.Time())

// registryセット前
// 2020-02-28 05:50:26.759 +0000 UTC
// 2020-02-28 14:50:26.759 +0900 JST

// registryセット後
// 2020-02-28 14:52:51.103 +0900 JST
// 2020-02-28 14:52:51.103 +0900 JST

なお、mapにデコードすると、primitive.DateTime でデコードされるのでJSTになるっぽい。

所感

ドキュメントに無いから、実装を追ってみた。

コード追うの大変だったので、ぜひドキュメントに追記いただきたい。。。

あと、設定してすぐに、v1.3.0でメソッド名が変わったので、コンボ食らった気分だった。。。

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

最も簡単にパワフルなGolangのvim開発環境をSpaceVimで構築する

はじめに

Vimでgolangを書く環境を整えたいと思っていたところ、SpaceVimを使ってそれがとても簡単にかつ満足度高く実現出来たので書いていきたいと思います。

前提としてSpaceVimが適用されていることが前提となりますので、インストールされていない方は、こちらで設定を行なってください。
Golangのインストールなどについても解説しません。

layersの追加

このlayerを使います。

~/.SpaceVim.d/init.tomlに以下の記述を追加します。

[[layers]]
  name = "lang#go"

一度保存して、vimを開いてください。
vimのnormalモードで以下を実行します。

:GoInstallBinaries

上記実行後、gotagsをインストールするため、以下もターミナルで実行しておきましょう。

$ go get -u github.com/jstemmer/gotags

使い方

key-bindingsに詳しい使い方は書いてありますが、以下に少し書いておきます。

キーバインド 実行内容
space + l + b go build
space + l + r go run
space + l + h go info
space + l + M add import
space + l + t go test
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

関数とメソッドの違い【golang】

オブジェクト指向言語では

関数

  • オブジェクト関係なく、呼ぶと何か処理をするプログラムの部品

メソッド

  • オブジェクト指向で登場する用語で、オブジェクトの動作を定義したものらしい。

golangはオブジェクト指向??

  • golangを勉強し始めて1週間。
  • 今まで、同義的に使っていたメソッドと関数という言葉・・・
  • golangがオブジェクト指向なのかはよくわからない(オブジェクト指向言語としてGolangをやろうとするとハマること)けど、まとめてみたらオブジェクト指向言語っぽく使えることを理解した。
  • とにかく、関数とメソッドは明確に違いがあるらしいとのことでまとめてみた
  • 諸先輩方、認識が違ったらご指摘お願いいたします。
  • ※比較しやすいよう同じ処理をする

関数

  • funcで始まる処理の塊の中で構造体(型)に紐づけられていないもの
main.go
type Square struct {
    X, Y int
}

func Area(s Square) int {
    return s.X * s.Y
}

func main() {
    s := Square{5, 5}
    fmt.Println(Area(s))  // >>25
}

メソッド

  • 構造体(型)に紐づく処理の塊
  • Area(s Square)の部分が↑の型に紐づくを実装している部分
  • こうすることでArea構造体:Squareの持ち物となり、s.Area()として呼び出すことができる。これがメソッド。
  • この紐づけた構造体(型)のことをレシーバ(receiver)と呼ぶらしい。
main.go
type Square struct {
    X, Y int
}

func Area(s Square) int {
    return s.X * s.Y
}
func main() {
    s := Square{5, 5}
    fmt.Println(s.Area())  // >>25
}

ポインタレシーバ

  • ↑のレシーバを使用して実態を書き換えることをポインタレシーバと呼ぶらしい。耳なじみのある言い方で言うと参照渡し
  • 例えば、SquareのArea()の前にSquareを拡張するScaleというメソッドを用意してみる。実態が変わるため出力は2500となる。
  • (s *Square) Scaleの部分のアスタリスク(*)を削除すると値レシーバ(値渡し)となり出力は25となるのでコチラでこのソースをコピペして試してみてください。
main.go
type Square struct {
    X, Y int
}

func (s Square) Area() int {
    return s.X * s.Y
}

func (s *Square) Scale(i int) {
    s.X = s.X * i
    s.Y = s.Y * i
}

func Area(s Square) int {
    return s.X * s.Y
}
func main() {
    s := Square{5, 5}
    s.Scale(10)
    fmt.Println(s.Area()) // >> 2500
}
  • とりあえず、関数とメソッドの違いでした。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ランダムに抽選する関数の確率テスト(Golang)

自作した社員抽選Slack Botの抽選確率がランダムになっているか調査するテストを書いてみました

Go + AWS LambdaでSlackの社員抽選botを作った話
https://blog.acall.jp/2019/11/develop-slack-lottery-bot/

テスト対象関数

main.go
package main

import (
    "math/rand"
    "time"
)


func lotteryOneUserFromUsers(userIDs []string) string {
    rand.Seed(time.Now().UnixNano())
    userID := userIDs[rand.Intn(len(userIDs))]
    return userID
}

スライスを渡すと要素の1つがランダムで返ってきます
とてもシンプルな関数です

テストコード

main_test.go
package main

import (
    "fmt"
    "math"
    "testing"
)

func Test_lotteryOneUserFromUsers(t *testing.T) {
    // initialize
    userIDs := []string{"a", "b", "c", "d", "e"}
    counts := make(map[string]int)
    for _, id := range userIDs {
        counts[id] = 0
    }

    // sampling
    const loopCnt int = 1000
    var i int
    var userID string
    for i < loopCnt {
        userID = lotteryOneUserFromUsers(userIDs)
        counts[userID]++
        i++
    }

    // calculation
    const allowErrRate = 0.05
    var probability float64
    var errRate float64
    expectProbability := float64(1) / float64(len(userIDs))

    fmt.Printf("ID: count, percentage\n")
    for k, v := range counts {
        probability = float64(v) / float64(loopCnt)
        errRate = math.Abs(expectProbability - probability)
        fmt.Printf("%s: %d ", k, v)
        fmt.Printf("%v per \n", probability*100.0)
        if errRate > allowErrRate {
            t.Errorf("Error Rate is too big. Label: %s, Error Rate: %f", k, errRate)
        }
    }
}

初期化

userIDs := []string{"a", "b", "c", "d", "e"}
counts := make(map[string]int)
for _, id := range userIDs {
    counts[id] = 0
}

userIDsに適当な引数定義しています
このuserIDsの抽選を何度も繰り返し、確率を求めます

countsは当選回数を格納するmapです

サンプリング

const loopCnt int = 1000
var i int
var userID string
for i < loopCnt {
    userID = lotteryOneUserFromUsers(userIDs)
    counts[userID]++
    i++
}

loopCntにループ回数を定義し、当選回数をcounts追加するループを回しています

確率の計算

const allowErrRate = 0.05
var probability float64
var errRate float64
expectProbability := float64(1) / float64(len(userIDs))

fmt.Printf("ID: count, percentage\n")
for k, v := range counts {
    probability = float64(v) / float64(loopCnt)
    errRate = math.Abs(expectProbability - probability)
    fmt.Printf("%s: %d ", k, v)
    fmt.Printf("%v per \n", probability*100.0)
    if errRate > allowErrRate {
        t.Errorf("Error Rate is too big. Label: %s, Error Rate: %f", k, errRate)
    }
}
変数 説明
probability 計算した確率
expectProbability 理想確率
errRate 計算した確率誤差
allowErrRate 許容確率誤差

確率の理想値と実際の確率の差を求めて許容確率誤差を超えていないか確認するだけです

実行

go test . -v でテストを実行してみます
-vはテストPASS時にも標準出力を表示するオプションです

$ go test . -v
=== RUN   Test_lotteryOneUserFromUsers
ID: count, percentage
a: 202 20.200000000000003 per
b: 205 20.5 per
c: 209 20.9 per
d: 212 21.2 per
e: 172 17.2 per
--- PASS: Test_lotteryOneUserFromUsers (0.01s)
PASS
ok      lottery 0.021s

PASSしてますね
許容確率誤差をもっと小さくしてみると失敗するようになります

許容誤差0.01の場合
$ go test . -v
=== RUN   Test_lotteryOneUserFromUsers
ID: count
c: 193 19.3 per
d: 219 21.9 per
e: 190 19 per
a: 182 18.2 per
b: 216 21.6 per
--- FAIL: Test_lotteryOneUserFromUsers (0.01s)
    handler_test.go:41: Error Rate is big. Label: d, Error Rate: 0.019000
    handler_test.go:41: Error Rate is big. Label: a, Error Rate: 0.018000
    handler_test.go:41: Error Rate is big. Label: b, Error Rate: 0.016000
FAIL
FAIL    lottery 0.021s
FAIL

まとめ

「このbot確率偏ってるんじゃない?」と聞かれた時はこの結果を見せて正しいロジックであることを証明したいと思います

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