はじめに
noctxは context.Context
なしでHTTPリクエストを送信しているコードを発見します。
もしあなたがライブラリ開発者であるなら context.Context
をHTTPリクエストに渡していないと、ライブラリを利用するユーザはHTTPリクエストのキャンセルやトレース情報の取得ができません。
そのためライブラリの中でHTTPリクエストを送信している場合には noctx
を利用することで、そのような問題を防ぐことができます。
特に会社からAPIのGo SDKを公開している場合には、noctxを利用することで多くのユーザが安心して利用することができるようになります。
##インストール方法
$ go install github.com/sonatard/noctx/cmd/noctx@latest
使い方
$ go vet -vettool=`which noctx` main.go
./main.go:6:11: net/http.Get must not be called
golangci-lintからの実行
noctx
を configで有効にする。
# デフォルトのlinterに加えてnoctxを追加する
linters:
enable:
- noctx
# もしくはすべてのlinterを有効にして不要なlinterを無効化する
linters:
enable-all: true
disable:
- xxx # 不要なlinter
# configに基づいた実行
$ golangci-lint run
# noctxだけを実行
golangci-lint run --disable-all -E noctx
検出のルール
- 以下の関数を実行している場合はエラー
- net/http.Get
- net/http.Head
- net/http.Post
- net/http.PostForm
- (*net/http.Client).Get
- (*net/http.Client).Head
- (*net/http.Client).Post
- (*net/http.Client).PostForm
- http.NewRequest 関数から返された http.Request を他の関数に渡しているとエラー
検出した場合の修正方法
- HTTPリクエストの送信では
(*http.Client).Do(*http.Request)
メソッドを利用する。 - Go 1.13以降では、
http.NewRequest
関数の代わりにhttp.NewRequestWithContext
関数を利用する。 - Go 1.12以前では、
http.NewRequest
のあとに(http.Request).WithContext(ctx)
を実行する。
(http.Request).WithContext(ctx)
はhttp.Request
をコピーして返すためパフォーマンスのデメリットがあります。もしあなたの作成したライブラリがGo1.13以降しかサポートしないのであれば http.NewRequestWithContext
を利用してください。
修正のサンプル
もしあなたがライブラリの作者で既にcontextを受け付けない関数を提供してしまっている場合は、別途contextを受け付ける関数を定義して、既存の関数はcontextを受け付ける関数のラッパーにすることをお勧めします。
// 修正前
// HTTPリクエストを送信しているが、contextを受け付けていない
func Send(body io.Reader) error {
req,err := http.NewRequest(http.MethodPost, "http://example.com", body)
if err != nil {
return nil
}
_, err = http.DefaultClient.Do(req)
if err !=nil{
return err
}
return nil
}
// 修正後
func Send(body io.Reader) error {
// SendWithContextを呼び出し、contextにはcontext.Background()を渡す
return SendWithContext(context.Background(), body)
}
// contextを受け付ける
func SendWithContext(ctx context.Context, body io.Reader) error {
// NewRequestをNewRequestWithContextに変更してcontextを渡す
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://example.com", body)
if err != nil {
return nil
}
_, err = http.DefaultClient.Do(req)
if err != nil {
return err
}
return nil
}
検出例
package main
import (
"context"
"net/http"
)
func main() {
const url = "http://example.com"
http.Get(url) // want `net/http\.Get must not be called`
http.Head(url) // want `net/http\.Head must not be called`
http.Post(url, "", nil) // want `net/http\.Post must not be called`
http.PostForm(url, nil) // want `net/http\.PostForm must not be called`
cli := &http.Client{}
cli.Get(url) // want `\(\*net/http\.Client\)\.Get must not be called`
cli.Head(url) // want `\(\*net/http\.Client\)\.Head must not be called`
cli.Post(url, "", nil) // want `\(\*net/http\.Client\)\.Post must not be called`
cli.PostForm(url, nil) // want `\(\*net/http\.Client\)\.PostForm must not be called`
req, _ := http.NewRequest(http.MethodPost, url, nil) // want `should rewrite http.NewRequestWithContext or add \(\*Request\).WithContext`
cli.Do(req)
ctx := context.Background()
req2, _ := http.NewRequestWithContext(ctx, http.MethodPost, url, nil) // OK
cli.Do(req2)
req3, _ := http.NewRequest(http.MethodPost, url, nil) // OK
req3 = req3.WithContext(ctx)
cli.Do(req3)
f2 := func(req *http.Request, ctx context.Context) *http.Request {
return req
}
req4, _ := http.NewRequest(http.MethodPost, url, nil) // want `should rewrite http.NewRequestWithContext or add \(\*Request\).WithContext`
req4 = f2(req4, ctx)
cli.Do(req4)
req5, _ := func() (*http.Request, error) {
return http.NewRequest(http.MethodPost, url, nil) // want `should rewrite http.NewRequestWithContext or add \(\*Request\).WithContext`
}()
cli.Do(req5)
}