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.md
と README_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 コマンドってなに?
JMESPath は JSON のクエリ言語の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 コマンドのインストール
Mac は brew で jp コマンドをインストールできる。
$ brew tap jmespath/jmespath $ brew install jmespath/jmespath/jp
jp コマンドの README は brew install jp
でインストールするように記載してるけど brew install jp
は https://github.com/sgreben/jp のほうをインストールしてしまうので、JMESPath を扱う jp コマンドのほうは brew install jmespath/jmespath/jp
でインストールする必要がある。
README を修正する PR を作ろうとしたらすでに同じ目的の PR があったのだけど2年前の PR がずっと放置されているみたいで、全体的にあまりメンテナンスしていないっぽい。 メンテナンスしてないコマンドを使っていくのは少し不安があるけど jp コマンドのリポジトリをフォークしてるリポジトリは結構あるみたいなので、そのへんはまた探してみるのがよさそう。
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.go
の MaxScanTokenSize
以上にになってしまうため。
// 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 コマンドをちょろっと使ってみたらすごく便利だったメモ。
GoReleaser は CLI と GitHub Actions が公開している。 個人的には GitHub Actions のほうを CI に取り込んで使うのがすごく便利に感じていて自分が作ってるツールとかは少しずつ GoReleaser を GitHub Actions のワークフローでやるようにしてるけど、GoReleaser がどんなものかは CLI のほうでいろいろ試してたら CLI のほうもすごく便利だった。
まだ GoReleaser を使ってないとかなら CLI でサッと試すのがお手軽だと思うし GitHub Actions じゃなくて CLI で手元のほうからなにかやるとかもありそうな気がするので CLI の goreleaser
コマンドをちょろっと使ってみたのをメモしておく。
GoReleaser はなにができるか
GoReleaser はこんなことができる。
- Go のプロジェクトをクロスコンパイルする
- アーティファクトを GitHub, GitLab と Gitea へリリースする
- Docker イメージとマニフェストを作成する
- Linux のパッケージと Homebrew のパッケージを作成する
自分は Go のプロジェクトは gox でクロスコンパイルして ghr で GitHub の 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
インストール方法
Mac は brew で 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.yml
が YAML の構文エラーのときはこんな感じにエラーがある行を表示する。
$ 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.yml
の dist
で指定できる。
# .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 にアップロードするときは GitHub の API トークンが必要になるので PAT を GITHUB_TOKEN
に設定しておく。もし PAT がまだ発行してなかったら https://github.com/settings/tokens/new で発行しておく。
$ export GITHUB_TOKEN=<GitHub の API トークン> $ goreleaser --rm-dist
GitHub の API トークンはファイルに保存しておいてファイルへのパスを .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:'