今回触ってみた新機能はこちら
- errors.Join()
- context.WithContextCause()
- reflect.Value.Comparable()
- strings.CutPrefix()
errors.Join()
Join returns an error that wraps the given errors. Any nil error values are discarded. Join returns nil if errs contains no non-nil values. The error formats as the concatenation of the strings obtained by calling the Error method of each element of errs, with a newline between each string.
ふむふむ。引数に渡した複数のerrorをラップして改行で繋いでくれるみたいですね。
渡したerrorがnilの場合は見てみぬふりをするみたいです。
公式の例を引っ張ってきました。
errors.Join()で複数エラーをラップ
package main
import (
"errors"
"fmt"
)
func main() {
err1 := errors.New("err1")
err2 := errors.New("err2")
err := errors.Join(err1, err2)
fmt.Println(err)
if errors.Is(err, err1) {
fmt.Println("err is err1")
}
if errors.Is(err, err2) {
fmt.Println("err is err2")
}
}
結果
err1
err2
err is err1
err is err2
確かに改行で連結されていて、errors.Is()
でも等価として判定されています。
しかしfmt.Errorf()
と違って、wrapError型ではなく普通のerror型なので、errors.Unwrap()
でもとのerrorを取り出すことはできなさそうです。
ちなみにfmt.Errorf()
は今までは1つのエラーしかラップできませんでしたが、1.20で複数のエラーをラップできるようになったみたいです。
fmt.Errorfで複数エラーをラップ
package main
import (
"errors"
"fmt"
)
func main() {
e1 := errors.New("error1")
e2 := errors.New("error2")
err := fmt.Errorf("errors: %w, %w", e1, e2)
fmt.Println(err)
}
結果(Go1.19以前)
errors: error1, %!w(*errors.errorString=&{error2})
結果(Go1.20)
errors: error1, error2
errors.Join()
はerrorを改行で繋ぐだけでエラーの詳細情報の付与とかはできないので、fmt.Errorf()
の劣化版なのでは?としか読み取れませんでした。。
context.WithCancelCause()
WithCancelCause behaves like WithCancel but returns a CancelCauseFunc instead of a CancelFunc. Calling cancel with a non-nil error (the "cause") records that error in ctx; it can then be retrieved using Cause(ctx). Calling cancel with nil sets the cause to Canceled.
WithCancel()と似ているけど、CancelFuncではなくCancelCauseFuncを返すみたいです。
今までエラーの内容はctx.Err()でCanceledエラーかDeadlineExceededエラーを取得していましたが、context.WithCancelCause()はそれに加えてCancelCauseFunc()に独自エラーを渡せばcontext.Cause()でそれを取得できるのでエラーの表現の幅が増えますね。
package main
import (
"context"
"fmt"
"time"
)
type myError struct{}
func (e *myError) Error() string {
return "DBに接続できませんでした"
}
func fn(ctx context.Context) {
fmt.Println("start")
defer fmt.Println("finish")
for i := 1; i <= 5; i++ {
select {
case <-ctx.Done():
fmt.Println(context.Cause(ctx))
return
default:
fmt.Printf("%ds\n", i)
time.Sleep(1 * time.Second)
}
}
}
func main() {
e := &myError{}
ctx, cancel := context.WithCancelCause(context.Background())
go fn(ctx)
time.Sleep(2 * time.Second)
cancel(e)
time.Sleep(3 * time.Second)
}
ちなみにWithDeadlineCause()やWithTimeoutCause()もありました。
reflect.Value.Comparable()
Comparable reports whether the value v is comparable. If the type of v is an interface, this checks the dynamic type.
reflectパッケージのValue型が比較可能な値なのかを判定してくれます。
比較可能な値とはintやstring、同じフィールドを持つ構造体、インターフェースなどですね。逆に比較できないのはスライスやマップ、関数などです。
比較可能性についての記事
package main
import (
"fmt"
"reflect"
)
func main() {
num := 1
fmt.Println(reflect.ValueOf(num).Comparable())
type User struct {
ID int
Name string
}
u := &User{1, "hoge"}
fmt.Println(reflect.ValueOf(u).Comparable())
f := func() {}
fmt.Println(reflect.ValueOf(f).Comparable())
}
strings.Cutprefix()
CutPrefix returns s without the provided leading prefix string and reports whether it found the prefix. If s doesn't start with prefix, CutPrefix returns s, false. If prefix is the empty string, CutPrefix returns s, true.
第二引数の文字列が第一引数の文字列の先頭に存在した場合、それを取り除いた文字列とtrueを返します(存在しなければfalse)。
strings.TrimPrefixも似たような挙動ですが、こちらは指定されたプレフィッ
クスが見つかったかどうかのboolは返ってきません。
package main
import (
"fmt"
"strings"
)
func main() {
s, ok := strings.CutPrefix("Hello,World!", "Hello,")
fmt.Println(s)
fmt.Println(ok)
s, ok = strings.CutPrefix("Hello,World!", "こんにちは、")
fmt.Println(s)
fmt.Println(ok)
}
ちなみにbytesパッケージにも同様の関数が存在し、末尾を削除するCutSuffix()もそれぞれ追加されたみたいです。