95
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

なぜContextを構造体に含めてはいけないのか、またそれが許される状況について #golang

Last updated at Posted at 2017-05-11

はじめに

Goのcontext.Context はリクエストスコープにおいてキャンセルの情報の伝播や値の受け渡しに利用するためのものですが、使う上でいくつかの注意が必要です。

Go Doc - Package contextに以下のような一文が存在します。

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:
Contextは構造体の中に保存せずに、Contextを必要としている関数に渡してください。コンテキストは変数名ctxとして第1引数に渡すべきです。

func DoSomething(ctx context.Context, arg Arg) error {
	// ... use ctx ...
}

ここには何故Contextを構造体に含めてはいけないかの理由が書いてありません。

その理由をGoogle社員の方の回答をもとに説明したいと思います。

Contextを構造体に含めてはいけない理由

構造体にContextを含めるということは、その構造体をリクエストスコープと紐付け、状態として管理することを意味します。
しかし状態を持つということは、別のリクエストスコープを持つContextからその構造体を利用する可能性がでてきてしまいます。

以下のようなコードを書いてしまうと想定していない挙動になります。

type S struct {
  ctx Context
}
func (s *S) Foo() {
  bar(s.ctx)
}

func bar(ctx context.Context) {
 go something()
 select {
    case <-ctx.Done():
        stopSomething()
    }
}

func Baz(ctx context.Context, s *S) {
  // ここでは引数に渡されたctxのリクエストスコープではなく、S構造体を作成したときのctxのリクエストスコープとして扱われてしまう。
  // Bazのctxがキャンセルされたとき、このs.Fooの呼び出しはキャンセルされないため、bar()の中のsomething()が動き続けてしまう。
  s.Foo()
}

もし正しくキャンセルの情報を伝播させるために以下のようにするべきです。

func (s *S) Foo(ctx context.Context) {
  bar(ctx)
}
func Baz(ctx context.Context, s *S) {
  // ctxを渡すことで、正しくsomething()を停止することができる。
  s.Foo(ctx)
}

func bar(ctx context.Context) {
 go something()
 select {
    case <-ctx.Done():
        stopSomething()
    }
}

Contextを構造体に含めることが許される状況

上記のように関数の第1引数にcontextを渡すことが理想的ですが、それができない状況が存在します。

それはio.Readerやio.Writerのようなインターフェースを満たさなければいけないようなケースです。このようなときにContextを渡してしまうとインターフェースを満たせなくなってしまいます。

しかし構造体にContextを含めた場合においても、先程説明した問題は回避しなければなりません。
Googleでは以下のようにして対応しているようです。

ctx := context.WithTimeout(context.Background(), 1*time.Second)
f, err := fs.Open(ctx, "file.go")
if err != nil {
	log.Fatal(err)
}
b, err := ioutil.ReadAll(f.IO(ctx))
package fs

type IO interface {
	io.ReadWriteSeeker
	io.ReaderAt
	io.Closer
}


type File struct {
	f *os.File
}

type fio struct {
	f   *os.File
	ctx context.Context
}

func Open(ctx context.Context, name string) (file *File, err error) {
	return OpenFile(ctx, name, os.O_RDONLY, 0)
}

func OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (file *File, err error) {
	defer interrupt(ctx)()
	f, err := os.OpenFile(name, flag, perm)
	if err != nil {
		return nil, err
	}
	return newFile(f), nil
}

func (f *File) IO(ctx context.Context) IO {
	return fio{f.f, ctx}
}

func (fio fio) Write(p []byte) (int, error) {
	defer interrupt(fio.ctx)()
	n := 0
	for len(p) > 0 {
		wn, err := fio.f.Write(p)
		n += wn
		p = p[wn:]
		err = errAgain(fio.ctx, err)
		if err != nil {
			return n, err
		}
		select {
		case <-fio.ctx.Done():
			return n, &os.PathError{
				Op:   "write",
				Path: fio.f.Name(),
				Err:  context.Canceled,
			}
		default:
		}
	}
	return n, nil
}

os.Fileではなくfs.Fileという独自の構造体を定義しています。これはio.ReadWriterを満たしていない構造体なので、ctxを利用せずにWrite()などを実行できないような設計になっています。Write()などを実行するためには必ずf.IO(ctx)としてfio構造体に返すことで利用することが可能になります。fio構造体はio.ReadWriterを実装しctxを格納します。

以下は、上記の方式でキャンセル可能なFileSystemパッケージを実装したサンプルです。

File system package that supports cancellation. - fs.go

個人的にContextを構造体に含めてもいいのではと思う状況

上記の問題点を理解することで、以下の2点を守れば構造体にContextを含めても問題が発生しないことがわかります。

  1. contextを含めた構造体はリクエストスコープ内だけで生存することを保証する。
  2. contextを含めた構造体のメソッドにはContextを渡さない。

問題がないと思う例

func handler(w http.ResponseWriter, r *http.Request) {
  ctx := context.Background()
  foo := &Foo{ctx:ctx}
  foo.GoodMorning()
  foo.GoodEvening()
  foo.GoodNight()
}

問題がある例

var foo *Foo

func handler(w http.ResponseWriter, r *http.Request) {
  ctx := context.Background()
  foo = &Foo{ctx:ctx}
  foo.GoodMorning()
  foo.GoodEvening()
  foo.GoodNight()
}

正しい例

var foo = &Foo{}

func handler(w http.ResponseWriter, r *http.Request) {
  ctx := context.Background()
  foo.GoodMorning(ctx)
  foo.GoodEvening(ctx)
  foo.GoodNight(ctx)
}

ただし問題がないと思う例では、ctxを渡して構造体を生成するためリクエストごとに構造体を作ることが必須になりパフォーマンスの点ではよくありません。

そのため問題がないと思う例を実施する動機が「メソッドの第1引数にcontextを渡すのが面倒」以外に存在しないので、積極的に実施する理由にはなりません。

おわり

## 参考

95
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
95
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?