Goに関する色々
gzip圧縮しながら、レスポンスを返す方法
Content-Encoding
を使用することで、Client <-> Server 間は圧縮されたレスポンスでやり取りできる。
ただし、pngファイルなどすでにdeflate圧縮されているものはContent-Encoding
を使っても転送時間をあまり短縮できないので、使い所が割と限定される。
package main
import (
"compress/gzip"
"io"
"log"
"net/http"
"os"
)
func main() {
http.HandleFunc("/gzip", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Encoding", "gzip")
f, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
gw := gzip.NewWriter(w)
defer gw.Close()
if _, err := io.Copy(gw, f); err != nil {
log.Fatal(err)
}
})
log.Println("start http server :8089")
log.Fatal(http.ListenAndServe(":8089", nil))
}
Goのmoduleでreplaceを使用する時、replace先が更にreplaceした場合、replace先のreplaceが適用されないっぽい。
https://golang.org/ref/mod#minimal-version-selection を読んで調査する
# ---> はreplace先
package1 ---> package2 ----> package3
上記の場合package1はpackage2がreplaceしているpakcage3のバージョンを見てくれない
https://github.com/skanehira/go-replace-example このリポジトリで再現可能
vim-jpのgoチャンネルでやり取りした内容をまとめる
-
こちら によると
replace
はメインモジュールのみ適用され、依存先は適用されない模様 -
replace
をするとバージョンが無視されるため、依存先のreplace
も適用してしまうとA, B => C
のような並列で依存するケースの場合、バージョンを決定できないという問題が起きる -
A => B => C
のような、直列で依存するケースは理論上可能だけど、「特定のケースのみreplace
可能」は混乱を招きやすいし、自明ではないので一律replace
できないとしたほうがシンプルでわかりやすい -
replace
はローカルで一時的に変更するために使用すべきで、依存先がreplace
していること自体はそもそも良くない状態
- シェルの環境変数とは別でGoが持つ環境変数がある
-
go env -w
でGoの環境変数を変更できるgo env -w GO111MODULE=auto
- 参考 https://text.baldanders.info/golang/go-env/
Goの環境変数を使うとなると、設定忘れが発生しそう
シェルの環境変数を使用すれば、dotfilesで管理できるから普通にシェルの方を使ったほうが良さそう
go version -m /path/to/cmd
でGoでビルドした実行ファイルのライブラリ依存が見れる
https://golang.org/ref/mod#go-version-m を参照
MacbookPro13% go version -m $(which docui)
/Users/skanehira/dev/go/bin/docui: go1.16beta1
path github.com/skanehira/docui
mod github.com/skanehira/docui (devel)
dep github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug=
dep github.com/docker/docker v0.7.3-0.20190111153827-295413c9d0e1 h1:5Z3Uksuiv0lpPslfRA25dYUV85hI+Pfvz/Pi1NM2wPA=
dep github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
dep github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
dep github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
dep github.com/gdamore/tcell v1.1.1 h1:U73YL+jMem2XfhvaIUfPO6MpJawaG92B2funXVb9qLs=
dep github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
dep github.com/lucasb-eyer/go-colorful v1.0.1 h1:nKJRBvZWPzvkwB4sY8A3U4zgqLf2Y9c02yzPsbXu/5c=
dep github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y=
dep github.com/opencontainers/go-digest v1.0.0-rc1 h1:WzifXhOVOEOuFYOJAW6aQqW0TooG2iki3E3Ii+WN7gQ=
dep github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
dep github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
dep github.com/rivo/tview v0.0.0-20190324182152-8a9e26fab0ff h1:GrQgx8/nVONecTx4oGQ6O78pD8lVehvpQqeHGGPrCQM=
dep github.com/rivo/uniseg v0.0.0-20190313204849-f699dde9c340 h1:nOZbL5f2xmBAHWYrrHbHV1xatzZirN++oOQ3g83Ypgs=
dep github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
dep golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 h1:FP8hkuE6yUEaJnK7O2eTuejKWwW+Rhfj80dQ2JcKxCU=
dep golang.org/x/sys v0.0.0-20190428183149-804c0c7841b5 h1:m0i9YywO9THhxmJvLEwKJDD/pD8ljCB+EaT/wYS41Is=
dep golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
MacbookPro13%
GoのCLIを作る時に使用するCI/CDを組み込んだテンプレートリポジトリ
記事にした https://zenn.dev/skanehira/articles/2021-04-22-go-cli-template
GoでS3に大容量なファイルをアップロードするときに気をつける点
aws-sdk-goを使って、S3にファイルをアップロードするには以下の方法がある
PutObject
のbodyはio.ReadSeeker
になっていて、これを実装している一部の構造体は以下とおり
*os.File
*bytes.Buffer
*strings.Reader
s3manager.Uploader
のbodyはio.Reader
になっていて、実装している一部の構造体とフィールドは以下とおり
*os.File
*bufio.Reader
*bytes.Buffer
*strings.Reader
http.Request.Body
S3に大容量なファイルをアップロードすることを考慮するとき
-
bytes.Buffer
とstrings.Reader
はメモリ上にファイルの中身を展開するので、メモリを消費するため大容量なファイルは向かない -
*os.File
はローカルのファイルを開くというケースに限定するので、front -> APIでS3にアップロードする場合は適切ではない
上記2点から、基本的に大容量なファイルをS3にアップロードする場合はPutObject
ではなく、io.Reader
が使えるs3manager.Uploader
を使用したほうがメモリ消費量を抑えられる場合がある
例えばAPIの場合http.Request.Body
はio.Reader
なので、そのままUploader
に渡せるためAPI内でバッファリングしなくて済む
ただし、s3manager.Uploader
はマルチパートアップロードなので、アップロード中に失敗した場合、断片がS3に残るので、削除といったハンドリングが必要
TODO:ベンチマークを取る、マルチパートの断片を削除の方法
aws-sdk-go以外にgo-cloudのblob
を使用してS3にアップロード可能
TODO:実際動かしてみる、io.Writerを取得しているようなので、io.Copy
が使えるためメモリ量を抑えられれそう
import (
"context"
"gocloud.dev/blob"
_ "gocloud.dev/blob/s3blob"
)
// blob.OpenBucket creates a *blob.Bucket from a URL.
bucket, err := blob.OpenBucket(ctx, "s3://my-bucket?region=us-west-1")
if err != nil {
return err
}
defer bucket.Close()
// Create a cancelable context from the existing context.
writeCtx, cancelWrite := context.WithCancel(ctx)
defer cancelWrite()
// Open the key "foo.txt" for writing with the default options.
w, err := bucket.NewWriter(writeCtx, "foo.txt", nil)
if err != nil {
return err
}
// Assume some writes happened and we encountered an error.
// Now we want to abort the write.
if err != nil {
// First cancel the context.
cancelWrite()
// You must still close the writer to avoid leaking resources.
w.Close()
}
参考
Goでの日付の扱うときに気をつけたい点(WIP)
- msecを取れるメソッドがないので、自前で用意する
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"time"
)
type MyTime struct {
time.Time
}
func Now() *MyTime {
return &MyTime{time.Now()}
}
func (m *MyTime) UnixMsec() int64 {
return m.UnixNano() / int64(time.Millisecond)
}
func (m *MyTime) Format(format string) string {
nsec := m.UnixMsec() * int64(time.Millisecond)
return time.Unix(0, nsec).Format(format)
}
func (m *MyTime) ISO8601() string {
return m.Format("2006-01-02T15:04:05.000Z07:00")
}
func (m *MyTime) MarshalJSON() ([]byte, error) {
return []byte(`"`+ m.ISO8601() + `"`), nil
}
func main() {
now := Now()
// output: 1619416601563
fmt.Println(now.UnixMsec())
// output: 2021-04-26T14:56:41.563+09:00
fmt.Println(now.ISO8601())
// output: "2021-04-26T14:56:41.563+09:00"
if err := json.NewEncoder(os.Stdout).Encode(now); err != nil {
log.Fatal(err)
}
}
参考
テストまわりで便利そうなパッケージ
https://github.com/docker/compose-cli のCIを読んでいたら、良さそうなテスト用パッケージを見つけた
gotestsum
インストール
MacbookPro13% go install gotest.tools/gotestsum@latest
go: downloading gotest.tools v1.4.0
go: downloading gotest.tools/gotestsum v1.6.4
go: downloading github.com/dnephin/pflag v1.0.7
go: downloading github.com/jonboulle/clockwork v0.2.2
go: downloading golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
go: downloading golang.org/x/mod v0.3.0
MacbookPro13%
gotestsum --format testname
でパッケージ名だけを出力する
MacbookPro13% go test -v
=== RUN TestObject
--- PASS: TestObject (0.00s)
=== RUN TestArray
--- PASS: TestArray (0.00s)
=== RUN TestVersion
--- PASS: TestVersion (0.00s)
=== RUN TestRun
--- PASS: TestRun (0.00s)
=== RUN TestIsKeyFile
--- PASS: TestIsKeyFile (0.00s)
PASS
ok github.com/skanehira/gjo 0.127s
MacbookPro13% gotestsum --format testname
PASS TestObject (0.00s)
PASS TestArray (0.00s)
PASS TestVersion (0.00s)
PASS TestRun (0.00s)
PASS TestIsKeyFile (0.00s)
PASS . (cached)
DONE 5 tests in 0.164s
MacbookPro13%
gotestsum --watch --format testname
でテストファイルに変更があった場合実行してくれる
MacbookPro13% gotestsum --watch --format testname
Watching 1 directories. Use Ctrl-c to to stop a run or exit.
Running tests in ./.
=== RUN TestObject
main_test.go:48: want "{\"a\":\"+123456\"}", but got "{\"a\":\"123456\"}"
--- FAIL: TestObject (0.00s)
FAIL TestObject (0.00s)
PASS TestArray (0.00s)
PASS TestVersion (0.00s)
PASS TestRun (0.00s)
PASS TestIsKeyFile (0.00s)
FAIL .
=== Failed
=== FAIL: . TestObject (0.00s)
main_test.go:48: want "{\"a\":\"+123456\"}", but got "{\"a\":\"123456\"}"
DONE 5 tests, 1 failure in 0.312s
ターミナルに通知を出す事もできる
- 依存ツールをインストールする必要がある
- Macのみ対応
MacbookPro13% go install gotest.tools/gotestsum/contrib/notify@latest
MacbookPro13% brew install terminal-notifier
Updating Homebrew...
==> Auto-updated Homebrew!
Updated 2 taps (homebrew/core and homebrew/cask).
==> New Formulae
mr2
==> Updated Formulae
Updated 43 formulae.
==> Updated Casks
Updated 6 casks.
==> Downloading https://ghcr.io/v2/homebrew/core/terminal-notifier/manifests/2.0.0
######################################################################## 100.0%
==> Downloading https://ghcr.io/v2/homebrew/core/terminal-notifier/blobs/sha256:d1268e236f13f5bb4cd5fead9cf54cfb54ceefb98e34861bd39cf3c7e6ef34cf
==> Downloading from https://pkg-containers-az.githubusercontent.com/ghcr1/blobs/sha256:d1268e236f13f5bb4cd5fead9cf54cfb54ceefb98e34861bd39cf3c7e6ef34cf?se=2021-04-21T23%3A35%3A00Z&sig=
######################################################################## 100.0%
==> Pouring terminal-notifier--2.0.0.arm64_big_sur.bottle.tar.gz
🍺 /opt/homebrew/Cellar/terminal-notifier/2.0.0: 12 files, 572.6KB
Goでの二分探索の実装
RDBMSの検索アルゴリズムで使われているB+Treeを理解する上、二分探索を理解する必要があるのでGoで実装をして、ベンチマークを取ってみた
ベンチマークはあえてnormalSearch
が一番時間掛かるパターンにして、最大どれくらいのベンチマークの差が出るかを確認した
実装のポイントは
- mid = (low + high) / 2
- mid < target の場合は low = mid + 1(イメージとしては配列が左部から縮まっていく)
- mid > target の場合は high = mid - 1(イメージとしては配列が右部から縮まっていく)
package main
import (
"sort"
"testing"
)
func setup() []int {
list := []int{1, 4, 55, 6, 57, 87, 100, 98, 29, 98, 11, 191, 283, 209, 1, 98}
sort.Ints(list)
return list
}
func normalSearch(list []int, t int) bool {
for _, i := range list {
if t == i {
return true
}
}
return false
}
func binarySarch(list []int, t int) bool {
var low, mid int
high := len(list) - 1
for {
if low > high {
return false
}
mid = (low + high) / 2
if list[mid] == t {
return true
}
if list[mid] < t {
low = mid + 1
} else {
high = mid - 1
}
}
}
func BenchmarkNormalSearch(b *testing.B) {
list := setup()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !normalSearch(list, 283) {
b.Fatal("not found")
}
}
}
func BenchmarkBinarySearch(b *testing.B) {
list := setup()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if !binarySarch(list, 283) {
b.Fatal("not found")
}
}
}
ベンチマーク
goos: darwin
goarch: arm64
BenchmarkNormalSearch-8 150674906 7.577 ns/op 0 B/op 0 allocs/op
BenchmarkBinarySearch-8 301391296 4.062 ns/op 0 B/op 0 allocs/op
PASS
ok _/private/var/folders/r7/7tvhlbjd7d3cwy8b4n4bbd300000gn/T/vNr3j52 3.811s
os.ReadFile が意外と奥深かった
-
os.ReadFile
はファイルから中身を読み取って返す関数 - 内部でやっている処理は以下
-
os.Open
でファイルを開く - ファイルサイズをチェックして、初期の
[]byte{}
のキャパを算出 -
[]byte{}
にファイルの中身を読み込みつつ、キャパが足りなくなったらappend
で拡張
-
上記の2の処理は次のコードになっている。
var size int
if info, err := f.Stat(); err == nil {
size64 := info.Size()
if int64(int(size64)) == size64 {
size = int(size64)
}
}
size++ // one byte for final read at EOF
// If a file claims a small size, read at least 512 bytes.
// In particular, files in Linux's /proc claim size 0 but
// then do not work right if read in small pieces,
// so an initial read of 1 byte would not work correctly.
if size < 512 {
size = 512
}
data := make([]byte, 0, size)
上記の処理でinfo.Size()
で返ってくるファイルサイズを一度int
にキャストしてからint64
に再度キャストして、キャスト前のサイズと比較している。
32bitマシンの場合、32bit以上のファイルサイズの値を扱えないので、その場合は512バイトのスライスをmakeする。
64bitマシンでは、intのサイズはint64と同じなので、if文は必ずtrueになる。
ただ、アーキテクチャに関係なく、大容量なファイルを扱う時にメモリが不足するとappend
の部分でpanicが起きる。
Windowsで結果はKoRoNさんから拝借しているが、メモリ不足な場合は次のようにpanicが発生する。
$ cat test.go
package main
func main() {
a := make([]byte, 0x40000000)
println(len(a))
a = append(a, 0)
println(len(a))
}
koron@fate ~/tmp
$ GOARCH=386 go run test.go
1073741824
runtime: out of memory: cannot allocate 1077936128-byte block (1077706752 in use)
fatal error: out of memory
goroutine 1 [running]:
runtime.throw(0xb28268, 0xd)
C:/Local/Go/current/src/runtime/panic.go:1117 +0x64 fp=0x11054eec sp=0x11054ed8 pc=0xaec924
runtime.(*mcache).allocLarge(0x9f0088, 0x40002000, 0x100, 0xb0501f)
C:/Local/Go/current/src/runtime/mcache.go:226 +0x250 fp=0x11054f1c sp=0x11054eec pc=0xacf8c0
runtime.mallocgc(0x40002000, 0x0, 0xb27500, 0x1)
C:/Local/Go/current/src/runtime/malloc.go:1078 +0x738 fp=0x11054f64 sp=0x11054f1c pc=0xac9e88
runtime.growslice(0xb1ec40, 0x31400000, 0x40000000, 0x40000000, 0x40000001, 0x0, 0x0, 0x0)
C:/Local/Go/current/src/runtime/slice.go:224 +0x102 fp=0x11054f9c sp=0x11054f64 pc=0xafda42
main.main()
C:/Users/koron/tmp/test.go:6 +0x90 fp=0x11054fc8 sp=0x11054f9c pc=0xb16610
runtime.main()
C:/Local/Go/current/src/runtime/proc.go:225 +0x237 fp=0x11054ff0 sp=0x11054fc8 pc=0xaeeb17
runtime.goexit()
C:/Local/Go/current/src/runtime/asm_386.s:1315 +0x1 fp=0x11054ff4 sp=0x11054ff0 pc=0xb11e71
exit status 2
このように、ファイルサイズの処理の部分はあくまでも32bitマシンを考慮した処理であり、事前にメモリが足りるかどうかを算出するためでないと思われる。
intのキャストでマイナスになることがある
ダウンキャスト(より小さなbitへのキャスト)で、値がマイナスになることがある。
例えば、int16 -> int8 キャストする時、上位8bitが削られて、下位8bitが再解釈された結果 -1 になることがある
package main
import (
"fmt"
)
func main() {
x := int16(0b0111111111111111)
fmt.Println(int8(x)) // -1
}
上記8bitが削られて11111111
になり、int(符号付きint)なので -1 になる
Goの正規表現をチェックできるサイト
7~9年前くらいから更新が止まっているっぽい?
CLIを自作したほうが良さそう?