Open15

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のバージョンを見てくれない

vim-jpのgoチャンネルでやり取りした内容をまとめる

  • こちら によるとreplaceはメインモジュールのみ適用され、依存先は適用されない模様
  • replaceをするとバージョンが無視されるため、依存先のreplaceも適用してしまうと A, B => Cのような並列で依存するケースの場合、バージョンを決定できないという問題が起きる
  • A => B => Cのような、直列で依存するケースは理論上可能だけど、「特定のケースのみreplace可能」は混乱を招きやすいし、自明ではないので一律replaceできないとしたほうがシンプルでわかりやすい
  • replaceはローカルで一時的に変更するために使用すべきで、依存先がreplaceしていること自体はそもそも良くない状態

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でS3に大容量なファイルをアップロードするときに気をつける点

aws-sdk-goを使って、S3にファイルをアップロードするには以下の方法がある

  1. PutObject
  2. s3manager.Uploader

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.Bufferstrings.Readerはメモリ上にファイルの中身を展開するので、メモリを消費するため大容量なファイルは向かない
  • *os.Fileはローカルのファイルを開くというケースに限定するので、front -> APIでS3にアップロードする場合は適切ではない

上記2点から、基本的に大容量なファイルをS3にアップロードする場合はPutObjectではなく、io.Readerが使えるs3manager.Uploaderを使用したほうがメモリ消費量を抑えられる場合がある
例えばAPIの場合http.Request.Bodyio.Readerなので、そのままUploaderに渡せるためAPI内でバッファリングしなくて済む

ただし、s3manager.Uploaderはマルチパートアップロードなので、アップロード中に失敗した場合、断片がS3に残るので、削除といったハンドリングが必要

TODO:ベンチマークを取る、マルチパートの断片を削除の方法

aws-sdk-go以外にgo-cloudblobを使用して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

参考記事:https://qiita.com/soy-curd/items/9f6fd0b8beca16084f04

os.ReadFile が意外と奥深かった

  • os.ReadFileはファイルから中身を読み取って返す関数
  • 内部でやっている処理は以下
    1. os.Openでファイルを開く
    2. ファイルサイズをチェックして、初期の[]byte{}のキャパを算出
    3. []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 になる

ログインするとコメントできます