全力で怠けたい

怠けるために全力を尽くしたいブログ。

awk のシステム変数のチートシートと使い方のメモ。

はじめに

awk のシステム変数はよく使うものとたまに使うものがあるけど、たまに使うものは結構忘れてしまって使うときになって調べるのが面倒くさいのでチートシートを作って、あと使い方の例をメモしてみた。

環境は Mac なのでそのへんのバージョンをメモしておく。

macOS のバージョン。

$ sw_vers | grep Product
ProductName:    macOS
ProductVersion: 11.4

awk のバージョン。

$ awk --version
awk version 20200816

awk のシステム変数

awk のシステム変数は2つのタイプがあって、1つはフィールドセパレータとかレコードセパレータみたいにデフォルト値を変更できる変数で、もう1つはカレントレコードのフィールド数とかカレントレコードの番号みたいに処理で利用できる変数がある。 2つのタイプの変数をそれぞれチートシートと使い方の例をメモしておく。

デフォルト値を変更できる変数

チートシート

デフォルト値を変更できる変数のチートシートをメモしておく。

変数名 意味 デフォルト値
FS フィールドセパレータ。
awk は入力をこの値でフィールドに分割する。
スペース
OFS 出力時のフィールドセパレータ。
awk はフィールドをこの値で分割して出力する。
スペース
RS レコードセパレータ。
awk は入力をこの値でレコードに分割する。
改行
ORS 出力時のレコードセパレータ。
awk はレコードをこの値で分割して出力する。
改行

FS

FS 変数はフィールドを分割する値を指定する。 FS 変数はデフォルトはスペースなのでレコードはスペースでフィールドに分割するけど、* (アスタリスク) でレコードを分割するならこんな感じ。

$ echo 'a*b*c' | awk 'BEGIN{FS="*"}{print $1, $2, $3}'
a b c

OFS

OFS 変数は出力時のフィールドセパレータを指定する。 OFS 変数はデフォルトはスペースなのでフィールドはスペースで区切って出力するけど、_ (アンダースコア) でフィールドを区切って出力するならこんな感じ。

$ echo 'a*b*c' | awk 'BEGIN{FS="*";OFS="_"}{print $1, $2, $3}'
a_b_c

OFS 変数は \n (改行) を指定すると改行でフィールドを区切って出力することもできる。

$ echo 'a*b*c' | awk 'BEGIN{FS="*";OFS="\n"}{print $1, $2, $3}'
a
b
c

RS

RS 変数はレコードを分割する値を指定する。 RS 変数はデフォルトは \n (改行) なので入力は改行でレコードに分割するけど、入力レコードがブロック状になってて空行で区切られてるレコードを分割するならこんな感じ。

$ cat rs.txt
Japan
日本
81

USA
アメリカ合衆国
1

$ awk 'BEGIN{RS=""}{print $1, $2, $3}' rs.txt
Japan 日本 81
USA アメリカ合衆国 1

ORS

ORS 変数は出力時のレコードセパレータを指定する。 ORS 変数はデフォルトは \n (改行) なのでレコードは改行で区切って出力するけど、---- でレコードを区切って出力するならこんな感じ。

$ awk 'BEGIN{RS="";ORS="\n----\n"}{print $1, $2, $3}' rs.txt
Japan 日本 81
----
USA アメリカ合衆国 1
----

処理で利用できる変数

チートシート

処理で利用できる変数のチートシートをメモしておく。

変数名 意味
NF カレントレコードのフィールド数
NR カレントレコードの番号
FNR 入力ファイルごとのカレントレコード番号
FILENAME カレント入力ファイルの名前。
入力がパイプのときは空になる。

NF

NF 変数はレコードが所定のフィールドがあるかをチェックするのに使える。 たとえばレコードが3つ以上のフィールドをもつときだけ出力するならこんな感じ。

$ cat nf.txt
a b c
a b
a b c d
a

$ awk 'NF>=3{print}' nf.txt
a b c
a b c d

NR

NR 変数は任意の番号のレコードだけ処理したりレコード番号を出力するのに使える。 たとえばファイルの内容にレコード番号を付与して出力するならこんな感じ。

$ awk '{print NR ":" $0}' nf.txt
1:a b c
2:a b
3:a b c d
4:a

FNR

NR 変数は awk に入力するすべてのレコードの通し番号になるけど、 FNR 変数は入力ファイルごとのレコード番号を出力するのに使える。 複数ファイルの内容にファイルごとのレコード番号を付与して出力するならこんな感じ。

$ cat a.txt
a
aa
aaa

$ cat b.txt
b
bb
bbb

$ cat c.txt
c
cc
ccc

# FNR 変数はファイルごとのレコード番号が入る
$ awk '{print FNR ":" $0}' a.txt b.txt c.txt
1:a
2:aa
3:aaa
1:b
2:bb
3:bbb
1:c
2:cc
3:ccc

# NR 変数はすべてのレコードでの通しのレコード番号が入る
$ awk '{print NR ":" $0}' a.txt b.txt c.txt
1:a
2:aa
3:aaa
4:b
5:bb
6:bbb
7:c
8:cc
9:ccc

FILENAME

FILENAME 変数は入力ファイルの名前を出力したりチェックするのに使える。 たとえばさきほどの FNR 変数の例だけどファイル名も出力するならこんな感じ。

$ awk '{print FILENAME ":" FNR ":" $0}' a.txt b.txt c.txt
a.txt:1:a
a.txt:2:aa
a.txt:3:aaa
b.txt:1:b
b.txt:2:bb
b.txt:3:bbb
c.txt:1:c
c.txt:2:cc
c.txt:3:ccc

参考

git diff で特定のパスの差分は表示しないやり方のメモ。

git diff で特定のパスの差分は表示しないやり方のメモ。

git diff コマンドは任意のコミット間での差分を表示するコマンドでこんな感じに使える。以下の例は差分が多くなるので --stat オプションを指定して差分があるファイルの名前だけ表示している。

$ git diff --stat 2d5ff09^..
 README.md       | 21 +++++++++++----------
 README_ja.md    | 21 +++++++++++----------
 api.go          | 18 ++++++++++--------
 graph.go        | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
 pixel.go        | 45 +++++++++++++++++++++++++++++++++++++++------
 user.go         | 22 +++++++++++++++++++---
 user_profile.go |  8 +++++++-
 webhook.go      | 30 ++++++++++++++++++++++++++----
 8 files changed, 194 insertions(+), 52 deletions(-)

git diff コマンドは差分を表示する対象のパスを指定するところで :(exclude)<パス> みたいにすると特定のパスの差分は表示しないようにできる。

たとえば :(exclude)README* を指定すると README.mdREADME_ja.md の差分が表示しなくなる。

$ git diff --stat 2d5ff09^.. -- ':(exclude)README*'
 api.go          | 18 ++++++++++--------
 graph.go        | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
 pixel.go        | 45 +++++++++++++++++++++++++++++++++++++++------
 user.go         | 22 +++++++++++++++++++---
 user_profile.go |  8 +++++++-
 webhook.go      | 30 ++++++++++++++++++++++++++----
 6 files changed, 172 insertions(+), 32 deletions(-)

(exclude) はタイプ数が多いけど (exclude) は短縮形の ! を使うとタイプ数はかなり減らせる。

$ git diff --stat 2d5ff09^.. -- ':!README*'
 api.go          | 18 ++++++++++--------
 graph.go        | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
 pixel.go        | 45 +++++++++++++++++++++++++++++++++++++++------
 user.go         | 22 +++++++++++++++++++---
 user_profile.go |  8 +++++++-
 webhook.go      | 30 ++++++++++++++++++++++++++----
 6 files changed, 172 insertions(+), 32 deletions(-)

以上。

JMESPath を CLI で試せる jp コマンドが便利。

JMESPath を CLI で試せる jp コマンドが便利なのでメモしておく。

JMESPath と jp コマンドってなに?

JMESPathJSON のクエリ言語の1つで JSON からデータを抽出したり集計するのに使う。

JMESPath は AWS CLI--query オプションが JMESPath での指定になるので知っておいて損はないとは思う。 自分は普段は AWS CLI の出力は jq コマンドでパースとフィルタリングすることがほとんどなのだけど、たまに jq コマンドがインストールしてなかったりインストールできない環境で AWS CLI の出力をパースしたりフィルタリングするときにすごく困ってしまう。

そこで JMESPath を CLI でいろいろ試せるコマンドを探したら jp コマンドというのがあるみたいで、使ってみたらかなり便利だったのでメモしておく。

jp コマンドのバージョン。

$ jp --version
jp version 0.1.3

jp コマンドのインストール

Macbrew で jp コマンドをインストールできる。

$ brew tap jmespath/jmespath
$ brew install jmespath/jmespath/jp

jp コマンドの READMEbrew install jp でインストールするように記載してるけど brew install jphttps://github.com/sgreben/jp のほうをインストールしてしまうので、JMESPath を扱う jp コマンドのほうは brew install jmespath/jmespath/jp でインストールする必要がある。

README を修正する PR を作ろうとしたらすでに同じ目的の PR があったのだけど2年前の PR がずっと放置されているみたいで、全体的にあまりメンテナンスしていないっぽい。 メンテナンスしてないコマンドを使っていくのは少し不安があるけど jp コマンドのリポジトリをフォークしてるリポジトリは結構あるみたいなので、そのへんはまた探してみるのがよさそう。

github.com

jp コマンドの使い方

基本的な使い方

jp コマンドは jp <オプション> <JMESPath の式> みたいにして実行する。

jp コマンドはこんな感じで標準入力から JSON を読みこんで処理する。

$ echo '[0, 1, 2, 3, 5]' | jp @
[
  0,
  1,
  2,
  3,
  5
]

出力をダブルクォートで囲まない

jp コマンドの最終的な出力が文字列のときはダブルクォート (") で囲むけど --unquoted オプションか -u オプションを指定するとダブルクォート (") で囲まなくなる。jp コマンドの出力をパイプでほかのコマンドに接続するときに使うやつ。

$ echo '{"name": "bob"}' | jp name
"bob"

$ echo '{"name": "bob"}' | jp -u name
bob

JP_UNQUOTED 環境変数を設定しておくことでも出力をダブルクォートで囲まなくなる。

$ export JP_UNQUOTED=true
$ echo '{"name": "bob"}' | jp name
bob

JSON をファイルから読み込む

--filename オプションか -f オプションで JSON をファイルから読み込む。

$ cat data.json
[0, 1, 2, 3, 5]

$ jp -f data.json @
[
  0,
  1,
  2,
  3,
  5
]

JMESPath の式をファイルから読み込む

--expr-file オプションか -e オプションで JMESPath の式をファイルから読み込む。

$ cat data.json
[0, 1, 2, 3, 5]

$ cat expr.txt
[::2]

$ cat data.json | jp -e expr.txt
[
  0,
  2,
  5
]

JMESPath のチュートリアルをやってみる

jp コマンドの手習いとして JMESPath の チュートリアル をやってみる。

  • Basic Expressions
  • Slicing
  • Projections
    • List Projections
    • Slice Projections
    • Object Projections
    • Flatten Projections
    • Filter Projections

Basic Expressions

$ echo '{"a": "foo", "b": "bar", "c": "baz"}' | jp a
"foo"

$ echo '{"a": {"b": {"c": {"d": "value"}}}}' | jp a.b.c.d
"value"

$ echo '["a", "b", "c", "d", "e", "f"]' | jp [1]
"b"

$ cat data.json
{"a": {
  "b": {
    "c": [
      {"d": [0, [1, 2]]},
      {"d": [3, 4]}
    ]
  }
}}

$ cat data.json | jp  a.b.c[0].d[1][0]
1

Slicing

$ echo '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]' | jp [0:5]
[
  0,
  1,
  2,
  3,
  4
]

$ echo '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]' | jp [5:10]
[
  5,
  6,
  7,
  8,
  9
]

$ echo '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]' | jp [:5]
[
  0,
  1,
  2,
  3,
  4
]

$ echo '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]' | jp [::2]
[
  0,
  2,
  4,
  6,
  8
]

$ echo '[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]' | jp [::-1]
[
  9,
  8,
  7,
  6,
  5,
  4,
  3,
  2,
  1,
  0
]

Projections

List and Slice Projections

$ cat data.json
{
  "people": [
    {"first": "James", "last": "d"},
    {"first": "Jacob", "last": "e"},
    {"first": "Jayden", "last": "f"},
    {"missing": "different"}
  ],
  "foo": {"bar": "baz"}
}

$ cat data.json | jp 'people[*].first'
[
  "James",
  "Jacob",
  "Jayden"
]

$ cat data.json | jp 'people[:2].first'
[
  "James",
  "Jacob"
]

Object Projections

$ cat data.json
{
  "ops": {
    "functionA": {"numArgs": 2},
    "functionB": {"numArgs": 3},
    "functionC": {"variadic": true}
  }
}

$ cat data.json | jp 'ops.*.numArgs'
[
  2,
  3
]

Flatten Projections

$ cat data.json
{
  "reservations": [
    {
      "instances": [
        {"state": "running"},
        {"state": "stopped"}
      ]
    },
    {
      "instances": [
        {"state": "terminated"},
        {"state": "running"}
      ]
    }
  ]
}

$ cat data.json | jp reservations[*].instances[*].state
[
  [
    "running",
    "stopped"
  ],
  [
    "terminated",
    "running"
  ]
]

Filter Projections

$ cat data.json
{
  "machines": [
    {"name": "a", "state": "running"},
    {"name": "b", "state": "stopped"},
    {"name": "b", "state": "running"}
  ]
}

$ cat data.json | jp "machines[?state=='running'].name"
[
  "a",
  "b"
]

Pipe Expressions

$ cat data.json
{
  "people": [
    {"first": "James", "last": "d"},
    {"first": "Jacob", "last": "e"},
    {"first": "Jayden", "last": "f"},
    {"missing": "different"}
  ],
  "foo": {"bar": "baz"}
}

$ cat data.json | jp 'people[*].first | [0]'
"James"

MultiSelect

$ cat data.json
{
  "people": [
    {
      "name": "a",
      "state": {"name": "up"}
    },
    {
      "name": "b",
      "state": {"name": "down"}
    },
    {
      "name": "c",
      "state": {"name": "up"}
    }
  ]
}

$ cat data.json | jp 'people[].[name, state.name]'
[
  [
    "a",
    "up"
  ],
  [
    "b",
    "down"
  ],
  [
    "c",
    "up"
  ]
]

$ cat data.json | jp 'people[].{Name: name, State: state.name}'
[
  {
    "Name": "a",
    "State": "up"
  },
  {
    "Name": "b",
    "State": "down"
  },
  {
    "Name": "c",
    "State": "up"
  }
]

Functions

$ cat data.json
{
  "people": [
    {
      "name": "b",
      "age": 30,
      "state": {"name": "up"}
    },
    {
      "name": "a",
      "age": 50,
      "state": {"name": "down"}
    },
    {
      "name": "c",
      "age": 40,
      "state": {"name": "up"}
    }
  ]
}

$ cat data.json | jp 'length(people)'
3

$ cat data.json | jp 'max_by(people, &age).name'
"a"

$ cat data.json | jp "myarray[?contains(@, 'foo') == \`true\`]"
[
  "foo",
  "foobar",
  "barfoo",
  "barfoobaz"
]

参考サイト

プログラムのちょっとした動作を確認するときはシェルが役に立つメモ。

Go を勉強している人が「Go の bufio パッケージのScanner.Err() に non nil の error を返させたい」と言ってたのでシェルを使ったら簡単にやれますよ、と話をしたときのメモ。

相手の人が書いてた Go のプログラムはこんな感じ。

// main.go
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    scanner := bufio.NewScanner(os.Stdin)

    for scanner.Scan() {
        fmt.Println(scanner.Text())
    }

    if err := scanner.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "failed to Scanner.Scan(): ", err)
    }
}

Scanner.Scan() で標準入力を読んだのをそのまま標準出力にプリントするプログラムなんだけど fmt.Fprintln(os.Stderr, "failed to Scanner.Scan(): ", err) のところを動かしてみたいらしい。

こういうのはシェルを使うと簡単に目的を達成できる。

これだけ。

$ yes | head -n 65536 | tr -d '\n' | go run main.go > /dev/null
failed to Scanner.Scan():  bufio.Scanner: token too long

# Go コードへの入力が1バイト少ないときはエラーにならないでちゃんと動く
$ yes | head -n 65535 | tr -d '\n' | go run main.go > /dev/null

ちなみに ↑ で 65536 バイトを指定するとエラーになるのは標準出力から Go コードへの入力が bufio/scan.goMaxScanTokenSize 以上にになってしまうため。

// bufio/scan.go
    MaxScanTokenSize = 64 * 1024

func (s *Scanner) Scan() bool {
// 省略
            if len(s.buf) >= s.maxTokenSize || len(s.buf) > maxInt/2 {
                s.setErr(ErrTooLong)
                return false
            }

Docker のイメージはマルチステージビルドしてイメージサイズを小さくするメモ。

はじめに

Docker のイメージはマルチステージでビルドしてイメージサイズを小さくするメモ。

ソースコードを Docker のなかでビルドしようとするとビルド時だけ必要なライブラリとかも Docker のイメージに含める必要があり、どうしても最終的な Docker イメージのサイズが大きくなってしまう。 あと Docker イメージの中間レイヤーが増えると最終的な Docker イメージのサイズが大きくなってしまうのでできるだけレイヤーの数を増やさない工夫をする必要があるのだけど、Docker Engine 17.05 で導入されたマルチステージビルドはこの2つの課題を一気に解決することができる。

マルチステージビルドのやり方

マルチステージビルドは FROM を Dockerfile の中に複数記述するとマルチステージビルドでき、Dockerfile のなかで最後に記述する FROM のステージが最終的な Docker イメージになる。

# ソースコードをビルドするステージ
FROM golang:1.16 AS BUILD

# ソースコードを実行して生成したバイナリを実行するステージ
FROM busybox

シングルステージビルドとマルチステージビルドでの Docker イメージのサイズを比較

Go のソースコードをシングルステージビルドとマルチステージビルドでそれぞれ Docker のなかでビルドして最終的な Docker イメージのサイズを比較してみる。

Go のソースコードはこんな感じで Hello World! を出力するだけのもの。

// app.go
package main

import "fmt"

func main() {
    fmt.Println("Hello World!")
}
// go.mod
module github.com/ebc-2in2crc/app

go 1.16

まずシングルステージビルドしていく。Dockerfile はこんな感じ。

FROM golang:1.16

WORKDIR /go/src/app
COPY . .

RUN go install -v ./...

CMD ["app"]
$ docker image build -t my-golang-app:single-stage-build .
$ docker container run --rm my-golang-app:single-stage-build
Hello World!

次にマルチステージビルドしていく。Dockerfile はこんな感じ。

FROM golang:1.16 AS BUILD
WORKDIR /go/src/app
COPY app.go .
COPY go.mod .
RUN GOOS=linux go build

FROM busybox
COPY --from=BUILD /go/src/app/app /usr/local/bin/
CMD ["app"]

マルチステージビルドは FROM のところで AS <ステージの名前> みたいにしてステージに名前をつけておくと、後続のステージは COPY --from=<ステージの名前> 〜 みたいにして先行するステージのイメージからファイルとかにアクセスできる。

$ docker image build -t my-golang-app:multi-stage-build .
$ docker container run --rm my-golang-app:multi-stage-build
Hello World!

当たり前だけどシングルステージビルドでもマルチステージビルドでも Docker コンテナを実行すると同じ結果になる。

シングルステージビルドで作った Docker イメージとマルチステージビルドで作った Docker イメージのサイズはこんな感じ。

$ docker image ls | grep my-golang-app
my-golang-app                      single-stage-build   46d618cbdd49   32 minutes ago   864MB
my-golang-app                      multi-stage-build    8dbd2f2d2e87   36 minutes ago   3.17MB

Docker のイメージはシングルステージビルドしたものが 864MB でマルチステージビルドしたものが 3.17MB とマルチステージビルドしたイメージのほうが圧倒的に小さくなってる。 マルチステージビルドのほうは busybox をベースイメージにしてるので非常に小さくなってるけど実際は distroless をベースイメージにすることのほうが多いと思う。 distroless は busybox に比べたらサイズは大きいけど手元でマルチステージビルドしてみたら Docker イメージは 21.1MB で golang:1.16 でシングルステージビルドしたものに比べたら40倍以上は小さくできている。

自分が作ってる Docker イメージはまだシングルステージビルドしているものがあるけどあえてシングルステージビルドする必要はないと思うし、いまは新しく作る Docker イメージは基本的にマルチステージビルドするようにしてる。

参考サイト

いまさら Go の context を少しだけ整理してみたメモ。

はじめに

Go で並行処理を実装するときはたいがい context を使うけど context はすごくよくできててフワッとした理解でもちゃんと使えてしまう。 いまさらだけど自分のなかで context とはなにかどうやって使うものかを少し整理したのでメモしておく。

context とは

context とはなにかだけど Go Concurrency Patterns: Context - The Go Blog の説明が分かりやすかったので引用しておく。

In Go servers, each incoming request is handled in its own goroutine. At Google, we developed a context package that makes it easy to pass request-scoped values, cancelation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.

(訳) Go のサーバーは受信した各リクエストをそれぞれの goroutine で処理します。 Google はリクエストスコープの値、キャンセルシグナル、デッドラインを API の境界を越えてリクエストの処理に関わるすべての goroutine に簡単に渡すことができる context パッケージを開発しました。

あとこのへん。

A Context is safe for simultaneous use by multiple goroutines. Code can pass a single Context to any number of goroutines and cancel that Context to signal all of them.

(訳) context は複数の goroutine が同時に安全に使える。コードは単一の context を任意の数の goroutine に渡すことができ、その context をキャンセルするとシグナルをすべての goroutine に送ることができる。

context ができること

context は大雑把にはこういうことができる。

  • 値を API の境界を超えて複数の goroutine に渡せる
  • context は木構造にでき、任意の context をキャンセルするとその context とその context の枝に連なるすべての Context をキャンセルできる

context の使い方

context をキャンセルする

キャンセルできる context は context.WithCancel() で作る。

context.WithCancel() の2番目の返り値の関数を呼び出すと1番目の返り値の context をキャンセルできる。

コードはこんな感じ。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go func() { run(ctx) }()

    time.Sleep(5 * time.Second)
    cancel()
    time.Sleep(1 * time.Second)
}

func run(ctx context.Context) {
    i := 1
    for {
        fmt.Printf("run(): %d count\n", i)
        i++
        select {
        case <-time.After(1 * time.Second):
            continue
        case <-ctx.Done():
            fmt.Printf("run(): canceled. reasone: %s\n", ctx.Err().Error())
            return
        }
    }
}

// run(): 1 count
// run(): 2 count
// run(): 3 count
// run(): 4 count
// run(): 5 count
// run(): canceled. reasone: context canceled

main() 関数は ctx, cancel := context.WithCancel(context.Background()) で context を作って run() 関数を新しい goroutine で動かす。

run() 関数は無限ループで1秒ごとに run(): 1 count みたいな文字列を出力する。 run() 関数に渡した context がキャンセルすると ctx.Done() が返すチャネルがクローズするので run() 関数 は return して終了する。

指定した時間が経過したら context をキャンセルする

指定した時間が経過したらキャンセルする context は context.WithTimeout() で作る。

context は context.WithTimeout() の2番目の引数に指定する時間が経過すると自動的にキャンセルする。 context.WithTimeout() の2番目の返り値の関数を呼び出すと context が自動的にキャンセルする前に context をキャンセルできる。

コードはこんな感じ。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    go func() { run(ctx) }()

    time.Sleep(10 * time.Second)
}

// run() 関数はさっきのと同じ

// run(): 1 count
// run(): 2 count
// run(): 3 count
// run(): 4 count
// run(): 5 count
// run(): 6 count
// run(): canceled. reasone: context deadline exceeded

main() 関数は ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) で context を作って run() 関数を新しい goroutine で動かす。

run() 関数は無限ループで1秒ごとに run(): 1 count みたいな文字列を出力する。 run() 関数に渡した context がキャンセルすると ctx.Done() が返すチャネルがクローズするので run() 関数 は return して終了する。

指定した日時になったら context をキャンセルする

指定した日時になったらキャンセルする context は context.WithDeadline() で作る。

context は context.WithDeadline() の2番目の引数に指定する日時になると自動的にキャンセルする。 context.WithDeadline() の2番目の返り値の関数を呼び出すと context が自動的にキャンセルする前に context をキャンセルできる。

コードはこんな感じ。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    deadline := time.Now().Add(5 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    go func() { run(ctx) }()

    time.Sleep(10 * time.Second)
}

// run() 関数はさっきのと同じ

// run(): 1 count
// run(): 2 count
// run(): 3 count
// run(): 4 count
// run(): 5 count
// run(): 6 count
// run(): canceled. reasone: context deadline exceeded

パッと見た感じは context.WithTimeout() と似てる。 実は context.WithTimeout() は内部で context.WithDeadline() を呼び出している。

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

context がキャンセルしたのをチェックする

Context 構造体の Done() メソッドが返すチャネルは context がキャンセルするとクローズするので context がキャンセルしたかをチェックできる。

select {
// context がキャンセルするとこの case に入る
case <-ctx.Done():
    // 終了処理
}

context がキャンセルした理由をチェックする

Context 構造体の Err() メソッドは context がキャンセルした理由を示す error を返すので context がキャンセルした理由をチェックできる。

たとえば Err() メソッドは context がキャンセル関数の呼び出しでキャンセルしたら context.Canceled を返すし、context がタイムアウトしてキャンセルしたら context.DeadlineExceeded を返す。

Err() メソッドは context がまだキャンセルしてないときに呼び出すと nil を返す。

context に値を持たせる

context.WithValue() 関数は値を context に持たせることができ、context が持っている値は Context 構造体の Value() メソッドで取り出せる。

値を context に持たせるコードはこんな感じ。

type key int

const k key = 0

type Values struct {
    msg string
}

// 省略
    v := Values{msg: "Hello World!"}
    ctx := context.WithValue(context.Background(), k, v)

context から値を取り出すコードはこんな感じ。 Value() メソッドは interface{} 型の値を返すので実際に使うときは型アサーションは必須。

   v := ctx.Value(k).(Values)
    fmt.Printf("%s\n", v.msg)

context のタイムアウトまで一定の時間を切ってたらなにもしない

Context 構造体の Deadline() メソッドは context がタイムアウトする日時の time.Time を返すので context があとどれくらいでタイムアウトするかチェックできる。

たとえば10秒くらいかかる処理を開始するときに context が5秒後にタイムアウトすることが分かってたら処理を開始しても処理が完了しないのは明らかなので、処理を開始しないでおくみたいなことができる。

Deadline() メソッドは context がタイムアウトしないときは2番目の返り値は false を返す。

Deadline() メソッドを使うコードの例はこんな感じ。run() 関数はタイムアウトまで2秒を切ってたらメッセージを出力して return する。

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    for i := 0; i < 5; i++ {
        go run(ctx)
        time.Sleep(1 * time.Second)
    }
}

func run(ctx context.Context) {
    deadline, ok := ctx.Deadline()
    if !ok {
        return
    }

    now := time.Now()
    d := deadline.Sub(now).Truncate(time.Second)
    if d < 2*time.Second {
        fmt.Printf("run(): do not run! time to deadline: %+v\n", d)
        return
    }

    fmt.Printf("run(): run! time to deadline: %+v\n", d)
}

// run(): run! time to deadline: 4s
// run(): run! time to deadline: 3s
// run(): run! time to deadline: 2s
// run(): do not run! time to deadline: 1s
// run(): do not run! time to deadline: 0s

参考サイト

goreleaser コマンドをちょろっと使ってみたらすごく便利だったメモ [GoReleaser]

はじめに

goreleaser コマンドをちょろっと使ってみたらすごく便利だったメモ。

GoReleaserCLIGitHub Actions が公開している。 個人的には GitHub Actions のほうを CI に取り込んで使うのがすごく便利に感じていて自分が作ってるツールとかは少しずつ GoReleaser を GitHub Actions のワークフローでやるようにしてるけど、GoReleaser がどんなものかは CLI のほうでいろいろ試してたら CLI のほうもすごく便利だった。

まだ GoReleaser を使ってないとかなら CLI でサッと試すのがお手軽だと思うし GitHub Actions じゃなくて CLI で手元のほうからなにかやるとかもありそうな気がするので CLIgoreleaser コマンドをちょろっと使ってみたのをメモしておく。

GoReleaser はなにができるか

GoReleaser はこんなことができる。

自分は Go のプロジェクトは gox でクロスコンパイルして ghrGitHub の Releases ページにアップロードしてるけどそのへんが GoReleaser で置き換えることができて、あと GoReleaser はクロスコンパイルの設定、アーティファクトの作成とか GitHub の releases へのアップロードの設定が YAML で設定できるのがとても便利で楽になってると感じる。

あと GoReleaser は動作がかなり速くて体感的にはクロスコンパイルとか一瞬で終わる感じ。

GoReleaser とかのバージョンをメモしておく。

GoReleaser のバージョン。

$ goreleaser --version
goreleaser version 0.164.0
commit: d822baf11f7773f6c02eeaf7e187157b335935b3
built by: homebrew

macOS のバージョン。

$ sw_vers | grep Product
ProductName:    macOS
ProductVersion: 11.3.1

インストール方法

Macbrew で GoReleaser をインストールできる。

$ brew install goreleaser

go install することもできる。

$ go install github.com/goreleaser/goreleaser

deb, rpm と apk でもインストールできる。このへんは 公式サイト のほうにやり方が書いてある。

goreleaser コマンドを使ってみる

.goreleaser.yml の雛形を作る

Git のリポジトリを作って適当な Go ファイルを作成する。

// main.go
package main

func main() {
    println("Hello World!")
}

goreleaser init コマンドを実行して .goreleaser.yml の雛形を作る。実際に使うときは自分の用途に合うように .goreleaser.yml を設定していく。

$ goreleaser init
   • Generating .goreleaser.yml file
   • config created; please edit accordingly to your needs file=.goreleaser.yml

goreleaser check コマンドを実行して .goreleaser.yml の設定が間違ってないかチェックする。

.goreleaser.ymlYAML の構文エラーのときはこんな感じにエラーがある行を表示する。

$ goreleaser check
   • loading config file       file=.goreleaser.yml
   ⨯ command failed            error=yaml: line 5: mapping values are not allowed in this context

YAML の構文エラーがないけど .goreleaser.yml の設定が間違ってるとこんな感じにエラーがある行を表示する。

$ goreleaser check
   • loading config file       file=.goreleaser.yml
   ⨯ command failed            error=yaml: unmarshal errors:
  line 10: field goosssss not found in type config.Build

.goreleaser.yml が問題なければこんな感じに表示する。

$ goreleaser check
   • loading config file       file=.goreleaser.yml
   • checking config:
      • snapshotting
      • github/gitlab/gitea releases
      • project name
      • loading go mod information
      • building binaries
      • creating source archive
      • archives
      • linux packages
      • snapcraft packages
      • calculating checksums
      • signing artifacts
      • docker images
      • artifactory
      • blobs
      • homebrew tap formula
      • scoop manifests
      • milestones
   • config is valid

とりあえず GoReleaser を動かしてみる

goreleaser --snapshot --skip-publish --rm-dist コマンドを実行してとりあえず GoReleaser を動かしてみる。

$ goreleaser --snapshot --skip-publish --rm-dist

GoReleaser は通常は Git のタグと一緒に使うけど --snapshot オプションを指定すると Git のタグは関係なく とりあえずロスコンパイルする。あと GoReleaser はデフォルトではアーティファクトGitHub とかにアップロードするけど --skip-publish オプションを指定するとアーティファクトGitHub とかにアップロードしない。 とりあえず動作と構成を確認したいけどアーティファクトGitHub とかにアップロードしたくないときはこの2つのオプションを指定して goreleaser コマンドを実行する感じ。

あと GoReleaser はデフォルトはアーティファクトdist ディレクトリに作成する & dist が空じゃないときはエラーになるけど、--rm-dist オプションを指定すると dist ディレクトリを削除してからクロスコンパイルする。 一応、アーティファクトの作成先のディレクトリは .goreleaser.ymldist で指定できる。

# .goreleaser.yml
dist: ./build

バージョンの埋め込み

GoReleaser はデフォルトで以下を ldflags に設定する。

  • main.version
  • main.commit
  • main.date

こんな感じのコードを書いておくとバージョンとかが main.version とかに埋め込む。

// main.go
package main

import "fmt"

var (
    version = "dev"
    commit  = "none"
    date    = "unknown"
    builtBy = "unknown"
)

func main() {
    fmt.Printf("my app %s, commit %s, built at %s by %s", version, commit, date, builtBy)
    // my app v0.0.0-next, commit none, built at 2021-05-16T15:10:15Z by gorelease
}

アーティファクトGitHub にアップロードする

GoReleaser がアーティファクトGitHub にアップロードするときは GitHubAPI トークンが必要になるので PAT を GITHUB_TOKEN に設定しておく。もし PAT がまだ発行してなかったら https://github.com/settings/tokens/new で発行しておく。

$ export GITHUB_TOKEN=<GitHub の API トークン>
$ goreleaser --rm-dist

GitHubAPI トークンはファイルに保存しておいてファイルへのパスを .goreleaser.yml で指定することもできる。

env_files:
  github_token: /path/to/your/github_token

すぐに使える .goreleaser.yml

.goreleaser.yml はだいたいこんな感じのものを個別のプロジェクトごとに設定をカスタマイズして使えそう。

before:
  hooks:
    - go mod tidy
builds:
  - env:
      - CGO_ENABLED=0
    goos:
      - linux
      - windows
      - darwin
    goarch:
      - 386
      - amd64
      - arm64
    ignore:
      - goos: darwin
        goarch: 386
      - goos: windows
        goarch: arm64
archives:
  # アーティファクトのアーカイブのフォーマット。
  # `tar.gz`, `tar.xz`, `gz`, `zip` and `binary` のいずれかを指定する。デフォルトは `tar.gz`
  - format: zip
    # アーティファクトのファイル名のテンプレート。
    # `myproject_darwin_amd64.zip` みたいなファイル名
    name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
    wrap_in_directory: true
checksum:
  name_template: 'checksums.txt'
snapshot:
  name_template: "{{ .Tag }}-next"
changelog:
  sort: asc
  filters:
    exclude:
      - '^docs:'
      - '^test:'

参考サイト