20200227のGoに関する記事は4件です。

自分なりのGoアプリケーションDockerfileのベストプラクティス

最終的に作成されるDocker Imageが軽量になること
Docker Buildにかかる時間を短縮する

を目標としている

フォルダ構成

$ tree .
.
├── Dockerfile
├── Makefile
├── pkg_a
│   ├── pkg_a.go
│   └── pkg_a_test.go
├── pkg_b
│   ├── pkg_b.go
│   └── pkg_b_test.go
├── go.mod
├── go.sum
├── main.go

標準的なGoアプリケーションの構成
ライブラリ管理はGo Modulesを使います。

Dockerfile

Dockerfileは以下のようになっています。
工夫点はコメントで番号つけてます。詳細は後述します

# ①
FROM golang:1.13 as builder

# ②
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64
WORKDIR /go/<アプリのリポジトリ名>

# ③
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# ④
RUN make

# ⑤
FROM alpine
RUN apk add --no-cache ca-certificates

# ⑥
COPY --from=builder /go/<アプリのリポジトリ名>/app /app
CMD ["/app"]

説明

① multi-stage buildsを使いDocker Imageの軽量化を行う

Dockerにはmulti-stage buildsという機能があり、これを使うと1つのDockerfile内で複数のFROMを指定し、ビルドのベースとなるImageを分けることができます。

これによりアプリケーションのビルド時のみ必要となるパッケージなどを、最終的なImageから省き、Imageサイズを削減するというのが簡単に行えます。

Goの場合、Goランタイムはビルド時のみ必要でバイナリにビルドして実行する場合は不要なので、最初のbuild用のImageベースはgolang, 最終的なImageのベースはalpineとしています。

Imageのサイズとしてはgolang:1.13が5.5GB、alpine:latestが2.68MBなので、そのままgolangを最終的なイメージベースとするより5GB以上のサイズ削減になります。

② Goのビルド設定

ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64

でビルドの設定を行なっています

特に重要なのがCGO_ENABLED=0で静的バイナリの生成を指定してます。
これ指定しないと、alpineで実行するときにエラーになります。

③ 依存ライブラリのダウンロードはなるべくキャッシュをきかせる

なるべくDocker build時にキャッシュをきかせて、ビルド速度を上げるために、リポジトリ内の全てのファイルをコピーする前に、Go Modules系のファイルだけコピーして依存ライブラリのダウンロードを行なっています。

Dockerのビルドは1行を1レイヤーとして考え、そのレイヤーで変更がなければキャッシュを利用し、変更があれば、それ以降のレイヤー全てがキャッシュ使われなくなります。

そのため実際は以下でも動くのですが、アプリのソースコードを修正しただけでも毎回依存ライブラリのダウンロードが発生してしまいます。

- COPY go.mod go.sum ./
- RUN go mod download
- COPY . .
- RUN make
+ COPY . .
+ RUN make

そのため、事前にgo.mod, go.sumだけをコピーしています。
この場合、依存ライブラリを新たに追加しない限りは、毎回キャッシュが使われてDocker buildにかかる時間を大幅に短縮できます。

④ Makefile使う

RUN go build -o appとかでもいいんですが、Makefileを用意しておくと、Dockerfileがスッキリして見栄えがよくなるかと思います。

ちなみにMakefileは以下のようになっています。

all: test build

setup:
    go get
test:
    go test ./...
build:
    go build -o app

⑤ 最終的なImageのベースはalpineを利用する

multi-stage buildについては前述の通りです。
mulit-stage buildで最終的なImageのベースとしては、サイズを意識するならalpine一択かなと思ってます。
(運用時にコンテナ内にexecで入ってデバッグ作業とかしたいなら別のImageとかの方が良いかもですが)

⑥ 別のステージで作成したアプリのバイナリファイルをコピーする

FROM golang:1.13 as builder
COPY --from=builder /go/<アプリのリポジトリ名>/app /app

FROM golang:1.13 as builderで最初のgolang:1.13を使ったbuildの間にbuilderというaliasをつけてます。

これを使ってCOPY --from=builder /go/<アプリのリポジトリ名>/app /appとすることで、golang:1.13でビルドしたアプリのバイナリファイルをalpineの中に持ってくることができます。

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

Go言語製 抽選Botの精度が怪しいので検証してみた話

いきさつ

ACALL株式会社では、slackで抽選をするbotがあって、会議のファシリテーターとか
誰か担当を決めるときにそのbotを使って決めています。

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

こんな感じで当選するのですが、
8c967525c648c218ea742cd5bfa515e8.png

このbotの精度がどうも怪しい。
私と弊社CTOがやたら当選する気がする!

d34c8bf56910f5a3db67231b5117a886.png

最初は「まあプログラムのランダム性ってそんなもんだよね。ハハハ」なんて言ってたもんですが、
なかなかの頻度で当選するので、これはもしやバグが寝てるのでは・・・・と思い、精度を検証してみました。

前提

  • goのバージョンは1.14rc1を使用

検証内容

抽出処理の確認

まず、抽選botのランダム処理は下記のようになっています。

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

userIDsには抽選対象のユーザーが入っているわけですが、この処理自体はシンプルで怪しいところはない。
というわけで、この処理を複数回実行して確率を算出します。

確認用コード

下記のようなコードを用意しました。

probability.go
package main

import (
  "math/rand"
  "time"
  "fmt"
  "strconv"
)

func main() {
  userIDs := []string{"CTO","EM","山田","鈴木","藤田","田中","中島","北川","西山","川口","遠藤"}
  var lottery_cnt map[string]int = make(map[string]int)
  const LOOP_CNT int = 1000
  //初期化
  for _, s := range userIDs {
    lottery_cnt[s] = 0
  }

  var userID string
  for i := 0; i < LOOP_CNT; i++ {
    userID = lotteryOneUserFromUsers(userIDs)
    lottery_cnt[userID] += 1
  } 

  fmt.Println("抽選実施回数は" + strconv.Itoa(LOOP_CNT) + "回です")
  fmt.Println("----------------------------")
  sum := 0
  for k, v := range lottery_cnt {
    fmt.Println(k + " : " + strconv.FormatFloat((float64(v)/float64(LOOP_CNT)*100.0), 'f', 1, 64) + "% (" + strconv.Itoa(v) + "回)")
    sum += v
  }
}

// 抽選処理実体
func lotteryOneUserFromUsers(userIDs []string) string {
    rand.Seed(time.Now().UnixNano())
  userID := userIDs[rand.Intn(len(userIDs))]
    return userID
}

LOOP_CNTで抽選する回数を設定しています。

いざ検証

さて、上記のコードを使ってまずは1,000回の抽選で検証してみます。

~/d/s/lottery ❯❯❯ go run probability.go
抽選実施回数は1000回です
----------------------------
山田 : 7.5% (75回)
鈴木 : 10.8% (108回)
西山 : 7.3% (73回)
遠藤 : 8.8% (88回)
中島 : 10.0% (100回)
北川 : 9.9% (99回)
川口 : 9.9% (99回)
CTO : 7.8% (78回)
EM : 10.1% (101回)
藤田 : 9.8% (98回)
田中 : 8.1% (81回)

ほうほう・・・それなりのゆらぎがあります。
ちなみにEMが私です。
理論上は回数を重ねれば重なるほど確率は均一になるはずなので、回数を増やします。
次は1万回でやってみます。

~/d/s/lottery ❯❯❯ go run probability.go
抽選実施回数は10000回です
----------------------------
遠藤 : 8.6% (856回)
EM : 9.4% (936回)
山田 : 9.2% (921回)
鈴木 : 9.1% (914回)
藤田 : 9.1% (908回)
中島 : 9.6% (960回)
川口 : 9.3% (927回)
CTO : 8.9% (889回)
田中 : 9.0% (900回)
北川 : 9.2% (921回)
西山 : 8.7% (868回)

前回よりも均一化されてきました。ほぼ9%前後です。
では一気に、50万件まで増やしてみます。

~/d/s/lottery ❯❯❯ go run probability.go
抽選実施回数は500000回です
----------------------------
北川 : 9.1% (45357回)
西山 : 9.1% (45277回)
川口 : 9.1% (45562回)
CTO : 9.0% (45103回)
山田 : 9.2% (45825回)
藤田 : 9.1% (45574回)
田中 : 9.1% (45283回)
EM : 9.2% (45765回)
鈴木 : 9.1% (45417回)
中島 : 9.1% (45630回)
遠藤 : 9.0% (45207回)

ほぼゆらぎはなくなりましたね!
50万回やれば問題ないと言えそうです。

が!

会社の抽選BOTで50万回もやるわけはない。
実際に弊社Slackの抽選bot利用回数を調べると163回の抽選が行われていました。
というわけで163回で検証してみます。

~/d/s/lottery ❯❯❯ go run probability.go
抽選実施回数は163回です
----------------------------
西山 : 9.2% (15回)
川口 : 12.9% (21回)
藤田 : 7.4% (12回)
中島 : 10.4% (17回)
北川 : 10.4% (17回)
鈴木 : 7.4% (12回)
田中 : 9.2% (15回)
遠藤 : 6.1% (10回)
CTO : 7.4% (12回)
EM : 10.4% (17回)
山田 : 9.2% (15回)

ふーむ・・・・多い人で12%。前後2〜3%程度のゆらぎですね。
母数が少ないとけっこうバラけます。

まとめ

slack上でのbot実行回数の総回数もすくないし、当選確率が均一化されないのは仕方ない。
よく当選する気がするのは気のせいかもしれない。気のせいでしょうきっと。
これからもこのbotは運用されるので、確率が均一化されていくのを期待します!

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

Golang製のSlackAPI(nlopes氏の)がレポジトリ引っ越したらしい(´・ω・`)

ちょうどSlackBotの開発をしていたところ気がついたのでメモ。

旧レポジトリ( 2020年2月27日時点 )

レポジトリURL: https://github.com/nlopes/slack

新レポジトリ( 2020年2月27日時点 )

レポジトリURL: https://github.com/slack-go/slack

README( nlopes/slack )

image.png

上記は2020年2月27日時点の nlopes/slack のREADMEですが以下の

but no guarantees are made on how up to date it will be

からとりあえずこれから入るアップデートが必要な機能とかは保証されないうんぬんでちゃんと新レポジトリから取った方が良さそうですね!

また、旧に nlopes/slack 使えなくなったらとりあえず新レポジトリから引っ張ってくれば良さそうですね :)

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

leet code 83 Remove Duplicates from Sorted List の覚え書き

83 Remove Duplicates from Sorted List
https://leetcode.com/problems/remove-duplicates-from-sorted-list/

この問題が曲者すぎた。
ぱっと見で簡単やろうと思った自分を殴りたい。激ムズでっせ。
ListNodeの値は[1,1,3]

func deleteDuplicates(head *ListNode) *ListNode {

    cur := head
    for cur != nil && cur.Next != nil {
        if cur.Val == cur.Next.Val {
            cur.Next = cur.Next.Next //これでheadも更新済み

            fmt.Println(head, "headそのもの")
            fmt.Println(head.Next, "head.Next")
            fmt.Println(cur.Next, "cur.Next")
        } else {
            cur = cur.Next
            //ここでcur自身を新しく作る(上書き)ので,curはheadとは別物になる
            //curという入れ物自体は変化する
            //しかし、入れた値のNextは番地を与えているので、中身を変えるとheadも変わる
        }
    }

    return head
}

ここで、考えなければならないことが大きく3つある。
ListNodeの扱い方
アルゴリズムを考える
ポインタ(番地とか)

その中でも、Listnodeとポインタが時間を取られたところ。

1 ListNode

//  * Definition for singly-linked list.
type ListNode struct {
    Val  int
    Next *ListNode
}

これはstruct(構造体)である。すでに定義済みのものを扱うとのこと
でもその中身とかどうやって見るのかわからん。調べてみた。
https://play.golang.org/p/StDSHvDEvb
&ListNode{Val: 2, Next: &ListNode{Val: 4, Next: &ListNode{Val: 3, Next: &ListNode{}}}}
中身はこうなっていて、2を取得するには、ListNode.Val、4を取得するにはListNode.Next.Valと書く。
ちなみに最後のNextは{0,nil}になるみたいです。⬅︎重要!

2 ポインタ
if分のprintを実行すると以下のようになる。

&{1 0xc000086050} headそのもの
&{3 0xc000086060} head.Next
&{3 0xc000086060} cur.Next
&{1 0xc000086050} headそのもの
&{3 0xc000086070} head.Next
&{0 <nil>} cur.Next

いかなる時でもheadの番地は変化していないのがわかる。
headの入れ物自体は変化はしないが、実際にはheadの中身(Next)は変化しているのもわかる。
head = [Val:1,Next{Val:1,Next:{Val:3,Next{}}}]
headの変数の入れ物自体は変化はしていない。これ→[]
headの中身head.Nextは変化しているので番地は変更される[Val:1,Next:{Val:3,Next{}}]

elseの処理の cur = cur.Nextはcurを上書きしている(つまり新しく作成しているので番地は変更になる)
けれども、その中身のNextの番地はheadのものなのでcur.Nextをいじるとhead.Nextも変更される。

という、大きな仮説を立てました。
よくわかる解説が欲しい。

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