Go1.20 New Features

2022/12/05に公開約11,400字

Go1.20 が2022年2月2日にリリースされ、そのリリースノートが公開されています。この記事ではその中から気になったものを抜粋し、いくつかの機能に関しては使用例も載せていきます。

https://golang.org/doc/go1.20

それでは見ていきましょう!

spec

slice から array への変更が可能になりました。
Go1.17 では slice から array の pointer に変換できる言語仕様が追加されましたが、slice から array に変換するには一度 pointer を経由しなければいけませんでした。Go1.20 からは直接 slice から array に変換できるようになります。

/*
  slice を要素数4の array に変換する
*/

x := []int{1, 2, 3, 4}

// Go1.20 より前
a1 := *(*[4]int)(x)

// Go1.20 以降
a2 := [4]int(x)

https://tip.golang.org/ref/spec#Conversions_from_slice_to_array_or_array_pointer


comparable で型制約された Generics を、「interface 型」や「interface 型を持つ spec comparable な複合型(struct 型など)」といった strictly comparable ではない型を型引数に取ってインスタンス化することができるようになりました。ただし、strictly comparable でないことにより、ランタイム時に panic になる可能性が出てきたことに注意が必要です。
以下のコードのコンパイルが通るようになります。

package main

import "io"

func F[T comparable]() {}

type I[T comparable] interface{ G(T) }

func main() {
	F[any]()
	F[io.Reader]()
	F[struct{ any }]()
	F[[1]any]()
	F[I[any]]()
}

spec comparable や strictly comparable などの用語についての説明は Nobishii さんの記事が詳しいです。
https://zenn.dev/nobishii/articles/basic-interface-is-comparable

go version

Go 製の Windows DLL や実行権限のない Linux 用バイナリからも go version -m で情報を取得できるようになりました。

go command

ビルド済の標準パッケージを $GOROOT/pkg に含まないことにより、Go ツールチェーンの圧縮ファイルのサイズが前リリースバージョンと比較して約2/3になりました。なおビルド済の標準パッケージは、3rdパーティパッケージと同様にビルド時にキャッシュされます。

https://github.com/golang/go/issues/47257


アーキテクチャごとにサブアーキテクチャを環境変数で指定することで、サブアーキテクチャをビルドタグとして認識できるようになりました。
https://github.com/golang/go/issues/45454

例えば、ARMv6 と v7 でビルドを分けたい場合は、ビルドする際に環境変数 GOARM=6GOARM=7 をそれぞれ指定すれば、ビルドタグとして arm.6arm.7 が使えるようになり、ビルド対象となるファイルを分けることができます。

https://pkg.go.dev/cmd/go@master#hdr-Build_constraints


go のサブコマンドに -C フラグが追加され、コマンドを実行するディレクトリを変更できるようになりました。


go generate に -skip フラグが追加され、パターンにマッチする //go:generate ディレクティブをスキップできるようになりました。
https://go-review.googlesource.com/c/go/+/421440


go test に -skip フラグが追加され、パターンにマッチするテスト関連の関数をスキップできるようになりました。
https://go-review.googlesource.com/c/go/+/421439


go build 時に -cover フラグをつけることで、統合テスト用のコードカバレッジを計測できるようになりました(Go1.20 ではプレビューサポートとなっているので、環境変数 GOEXPERIMENT=coverageredesign を設定する必要があります)。使い方については、以下のページに詳しく記載されているのでご覧ください。
https://go.dev/testing/coverage/

cgo

C ツールチェーンのない環境では、環境変数 CGO_ENABLED がデフォルトで無効になったため、明示的に有効にしない場合は Go ベースの標準パッケージを使用するようになりました。C ツールチェーンがない環境とは、具体的には、環境変数 CGO_ENABLEDCC が明示的に設定されていない、かつ gcc や clang などの C コンパイラが PATH で見つからない環境です。ちなみに cgo を使用している標準パッケージは net, os/user, plugin です。

macOS では完全に cgo を使わないように再実装されたので、CGO_ENABLED を有効にしても Go ベースの標準パッケージを使用するようになります。他の OS では、CGO_ENABLED を有効にした場合は今まで通り cgo を使用した標準パッケージが使用できます(特に理由がない限りは Go ベースのほうを使ったほうが良さそうです)。


macOS 用の race detector は cgo を使わない Go ベースのものに再実装されたため、Xcode なしで race detector を実行できるようになりました。

vet

ループ変数が関数に多重にネストされている場合でもgo vet で検知するようになりました。
https://go-review.googlesource.com/c/tools/+/452155


time layout が ISO8601 の形式 2006-01-02(yyyy-mm-dd) ではなく、2006-02-01(yyyy-dd-mm) になっていた場合、go vet で検知するようになりました。
https://github.com/golang/go/issues/48801

compiler

Profile-Guided Optimization(PGO) がプレビューサポートされました。Go バイナリにビルドする際の最適化処理をビルド時だけで完結するのではなく、ランタイム時のプロファイル情報を収集して次回以降のビルドに活かす、コンパイラ最適化の手法の一つです。

内部的には、pprof を使用しており、Go 1.20 時点では CPU プロファイルがサポートされます。これにより、頻繁に呼び出す関数やメソッドを積極的にインライン化するようになります。

今後は CPU 以外のプロファイルもサポートされていく予定です。

PGO をオンにするには go build, go install, go run などビルド系のコマンドで -pgo フラグを有効にする必要があります。

-pgo=auto と指定することで、main package にある default.pgo を検索して、存在すれば使用します。-pgo=<filepath> と指定することで、ファイルパスを明示することもできます。

詳しくはユーザーガイドが公開されているので、ご覧ください。

https://go.dev/doc/pgo

https://github.com/golang/go/issues/55022


次の型定義のように循環して interface を定義すると、ビルドエラーになるようになりました。
なお後方互換性が崩れる変更となるため、保ちたい場合はビルド時に -d=interfacecycles を指定してください。
もし悪影響がない場合、Go 1.22 で言語仕様として正式にビルドエラーになります。

type I interface { m() interface { I } }

https://go-review.googlesource.com/c/go/+/445598


Go1.18 で Generics が導入されビルド時間が長くなっていましたが、Go1.20 では Go1.19 に比べ9%ほど高速化され、Go1.17 並の速度に戻りました。

https://github.com/golang/go/issues/49569#issuecomment-1382373680

linker

Linux では、ベースとなる C ライブラリが glibc と musl libc とが存在しており、ビルド時に参照する Dynamic Interpreter(ld.so等)が異なるため、Go ツールチェーンはそれぞれ用意されていました。Go 1.20 からはビルド時に Dynamic Interpreter を自動検知するするようになったため、1つの Go ツールチェーンを用意するだけで良くなりました。この変更で影響があるのは、例えば musl ベースの Linux ディストリビューションである Apline Linux を Docker Base Image にし、Go をインストールして独自の Docker Image 使っている方です(恐らくソースコードをダウンロードしてきて make.bash を実行してインストールされていると思うのですが、https://go.dev/dl/go${VERSION}.linux-${GOARCH}.tar.gz がそのまま使えるようになります)。
https://go-review.googlesource.com/c/go/+/420774


macOS 上で DNS による名前解決を行う際に、Go ビルトインの機能ではなく macOS ネイティブの Resolver を使うようになりました。
これにより、macOS 以外の環境で macOS 向けにクロスコンパイルを行った Go バイナリでも、macOS 上で動作させる際に、macOS ネイティブの Resolver の挙動と差異なく動作させることができます。
https://go-review.googlesource.com/c/go/+/446178

bootstrap

Go1.20 のデフォルトのブートストラップツールチェーンとして Go1.17.13 が使用されるようになりました。
https://github.com/golang/go/issues/44505

package

arena

arena package が新しく実験的にサポートされました。
Garbage Collection(GC) の影響を受けずにメモリを確保できる機構です。GC に使用していた CPU リソースを他の処理に活かせるようになるため、確保したメモリを長時間解放せず、GC される可能性が明らかにないと分かっている場合に使用すると良いかもしれません。
具体的な使い方に関しては、後日アドカレで記事にする予定なのでお楽しみに!
https://github.com/golang/go/issues/51317

bytes, strings

bytes package に関数 CutPrefix, CutSuffix が追加されました。
バイト列 A の先頭や末尾からバイト列 B を取り除いたバイト列 C を返せるようになります。動作は TrimPrefix, TrimSuffix と同じですが、CutPrefix, CutSuffix は第2戻り値として取り除けた場合は true, 取り除けなかった場合は false を返す点で異なります。

さらに、strings package にも同様の機能が追加されました。
https://go-review.googlesource.com/c/go/+/407176

bytes

bytes package に関数 Clone が追加されました。
バイト列をクローンして返せるようになります。
https://go-review.googlesource.com/c/go/+/359675

context

context package に関数 WithCancelCause, Cause が追加されました。
WithCancelCause が返す context.Context がすでにキャンセル済みである場合エラーを、キャンセルされていない場合 nil を返すことで、キャンセル済みかどうかを判断できるようになります。
https://go-review.googlesource.com/c/go/+/375977

errors, fmt

複数の error を同時に Wrap できるようになりました。

方法としては以下の2種類があります。

  1. fmt.Errorf の format string に Wrap したい error の数だけ %w を用いる。
  2. 新しく追加された関数 errors.Join を用いる。
err1 := errors.New("err1")
err2 := errors.New("err2")

// 複数の %w を用いる場合
err3 := fmt.Errorf("err: %w, %w", err1, err2)

// errors.Join を用いる場合
err4 := errors.Join(err1, err2)

同時に Wrap する際の戻り値は、interface{ Unwrap() []error } を実装することで errors.Iserrors.Us できちんと捕捉できるようになります。ただし、errors.Unwrap で Unwrap する際は interface{ Unwrap() error } を実装していないことになるので nil が返ってくることになります。

fmt.Println(errors.As(err3, &err1), errors.As(err3, &err2))
fmt.Println(errors.As(err4, &err1), errors.As(err4, &err2))
fmt.Println(errors.Unwrap(err3))
fmt.Println(errors.Unwrap(err4))
// Output:
// true true
// true true
// <nil>
// <nil>

Go Playground

fmt

fmt package に関数 FormatString が追加されました。
関数 fmt.Printf などで用いられるフォーマット文字列を、インターフェース fmt.State を用いてプログラマブルに生成できるようになります。
https://github.com/golang/go/issues/51668

go/types

go/types package に関数 Satisfies が追加されました。
型が指定した型制約を満たしているか確認する際に使うことができます。
なお、型が指定した interface を実装しているかの確認には使用できません。既存の関数 Implements を使いましょう。

https://go-review.googlesource.com/c/go/+/454575

io

io package に構造体 OffsetWriter が追加されました。
https://github.com/golang/go/issues/45899

math/rand

math/rand package の関数 Read, Seed は非推奨になりました。
以前の math/rand package の package global な乱数生成器は、シードを定めないまま乱数を生成するとシードに1を設定したものとして初期化されていました。
Go1.20からは、math/rand package をインポートした時点でランダムにシード値が定められるようになりました。ただ、上述の通り非推奨になったため、乱数を生成する際は個別に乱数生成器を初期化して使用しましょう。

Go1.20 より前
https://github.com/golang/go/blob/go1.19.3/src/math/rand/rand.go#L293

Go1.20 以降
https://github.com/golang/go/blob/ad7dc8ad55e6540bbf285df869d4ee6c12fff0e7/src/math/rand/rand.go#L304

https://github.com/golang/go/issues/56319

net/http

net/http package に構造体 ResponseController とそれを初期化する関数 NewResponseController が追加されました。NewResponseController の引数に渡す ResponseWriter が以下のメソッドを実装している場合、そのメソッドを透過的に呼ぶことができます。

Flush()
FlushError() error // alternative Flush returning an error
Hijack() (net.Conn, *bufio.ReadWriter, error)
SetReadDeadline(deadline time.Time) error
SetWriteDeadline(deadline time.Time) error

特に SetReadDeadlineSetWriteDeadline によって、リクエストごとに読み書きのデッドラインを設定できるようになります。


net/http package の構造体 Server にフィールド DisableGeneralOptionsHandler が追加されました。リクエスト "OPTIONS *" が来た時に、このフィールドが true の場合は対応する Handler が処理を行い、false の場合は Handler で処理を行わずに「Content-Length:0 の 200 OK」を返すようになります。


net/http package の HTTP サーバーでは、ボディを含む HEAD リクエストも受けつけるようになりました。
https://go-review.googlesource.com/c/go/+/418614


Cookie 名の前後に空白が含まれていても無視されずに、トリムされた値として受け入れられるようになりました。
例えば Cookie が "name =value" の場合、Cookie 名は "name" となります。
https://go-review.googlesource.com/c/go/+/397734

os/exec

os/exec package の構造体 Cmd にフィールド CancelWaitDelay が追加されました。
https://pkg.go.dev/os/exec@master#Cmd

reflect

reflect package の構造体 Value にメソッド ComparableEqual が追加されました。
2つの値が == で比較したときに true だった場合は Equal は同じく true を返し、false の場合も同様に false を返します(実際のところ条件はもう少し複雑なので、詳しくは GoDoc をご覧ください)。
Comparable はそもそも値が == で比較可能かを示します。


reflect package の構造体 Value にメソッド Grow が追加されました。
Value.Kind() == Kind.Slice の場合に限り、必要な分だけ cap を増やした後、引数の n だけ len を増やします。
Value.Kind() != Kind.Slice の場合や、n が負や大きすぎる場合は panic を起こします。


reflect package の構造体 Value にメソッド SetZero が追加されました。
Value.Kind() に対応するゼロ値を付与します。

regexp

正規表現パーサーがメモリを大量に消費する場合、Go1.19.2 と Go1.18.7 で入ったパッチにより syntax.ErrInternalError を返していましたが、Go1.20 からはエラー内容をより明確にするために syntax.ErrLarge を返すようになりました。

sync

sync package の構造体 Map にメソッド Swap, CompareAndSwap, CompareAndDelete が追加され、より厳密なアトミック処理が行えるようになりました。
https://github.com/golang/go/issues/51972

time

time package に time layout に使える定数 DateTime, DateOnly, TimeOnly が追加されました。

DateTime   = "2006-01-02 15:04:05"
DateOnly   = "2006-01-02"
TimeOnly   = "15:04:05"

time package の構造体 Time にメソッド Compare が追加されました。
「以前」や「以降」を判定したい場合、今までは Before/AfterEqual の2つを組み合わせて呼んでいましたが、Compare の戻り値を比較するだけで良くなりました。

/*
  現在時刻 now が有効期限 expiredAt 以降である場合 true
*/

expiredAt := time.Date(2009, 11, 10, 19, 00, 0, 0, time.UTC)
now := time.Now()

// Go1.20より前
fmt.Println(now.Equal(expiredAt) || now.After(expiredAt))

// Go1.20以降
fmt.Println(now.Compare(expiredAt) >= 0)

Go Playground

strconv

strconv package の関数 ParseXXX[]bytestring に変換して渡す際に、内部で明示的に string をコピーすることによって、コンパイラのエスケープ解析機能を利用してヒープへのメモリアロケーションを行わないようにできるようになったため、パフォーマンスが改善されました。
https://go-review.googlesource.com/c/go/+/345488

unsafe

unsafe package に関数 SliceData, String, StringData が追加されました。
これにより、より直感的に slice と string を unsafe に扱えるようになります。

/*
  string を []byte にコピーせずに変換する
*/

s := "hello, world!"

// Go1.20 より前
b1 := *(*[]byte)(unsafe.Pointer(&s))

// Go1.20 以降
b2 := unsafe.Slice(unsafe.StringData(s), len(s))

https://github.com/golang/go/issues/53003


Applibot Advent Calendar 2022
Go Advent Calendar 2022

Discussion

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