Go1.20の新機能を少し触ってみた

今回触ってみた新機能はこちら

- errors.Join()
- context.WithContextCause()
- reflect.Value.Comparable()
- strings.CutPrefix()


## [errors.Join()](https://cs.opensource.google/go/go/+/go1.20.2:src/errors/join.go;l=13)
> 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()で複数エラーをラップ

```go
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で複数エラーをラップ
```go
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()](https://cs.opensource.google/go/go/+/master:src/context/context.go;l=263)

> 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()でそれを取得できるのでエラーの表現の幅が増えますね。

[playground](https://go.dev/play/p/5X9MPuymiRP)
```go
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()](https://pkg.go.dev/context@master#WithCancelCause:~:text=func-,WithDeadlineCause,-%C2%B6)や[WithTimeoutCause()](https://pkg.go.dev/context@master#WithCancelCause:~:text=func-,WithTimeoutCause,-%C2%B6)もありました。

## [reflect.Value.Comparable()](https://pkg.go.dev/reflect#Value.Comparable:~:text=func%20(Value)-,Comparable,-%C2%B6)
> 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、同じフィールドを持つ構造体、インターフェースなどですね。逆に比較できないのはスライスやマップ、関数などです。
[比較可能性についての記事](https://qiita.com/tenntenn/items/e15cc0c54b3bbfddb04e)

[playground](https://go.dev/play/p/D5OnvQFRERB)
```go
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()](https://pkg.go.dev/strings#CutPrefix:~:text=func-,CutPrefix,-%C2%B6)
>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](https://pkg.go.dev/strings#CutPrefix:~:text=func-,TrimPrefix,-%C2%B6)も似たような挙動ですが、こちらは指定されたプレフィッ
クスが見つかったかどうかのboolは返ってきません。

[playground](https://go.dev/play/p/1Db7zAaAV5b)
```go
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()もそれぞれ追加されたみたいです。