context.ContextなしでHTTP Requestを送信しているコードを発見する `noctx`

## はじめに

https://github.com/sonatard/noctx/

noctxは `context.Context` なしでHTTPリクエストを送信しているコードを発見します。

もしあなたがライブラリ開発者であるなら `context.Context` をHTTPリクエストに渡していないと、ライブラリを利用するユーザはHTTPリクエストのキャンセルやトレース情報の取得ができません。

そのためライブラリの中でHTTPリクエストを送信している場合には `noctx` を利用することで、そのような問題を防ぐことができます。

特に会社からAPIのGo SDKを公開している場合には、noctxを利用することで多くのユーザが安心して利用することができるようになります。

##インストール方法

```sh
$ go install github.com/sonatard/noctx/cmd/noctx@latest
```

## 使い方

```sh
$ go vet -vettool=`which noctx` main.go
./main.go:6:11: net/http.Get must not be called
```

## golangci-lintからの実行

`noctx` を configで有効にする。

```yaml:.golangci.yml

# デフォルトのlinterに加えてnoctxを追加する
linters:
  enable:
    - noctx

# もしくはすべてのlinterを有効にして不要なlinterを無効化する

linters:
  enable-all: true
  disable:
   - xxx # 不要なlinter
```

```sh
# 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を受け付ける関数のラッパーにすることをお勧めします。

```go
// 修正前
// 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
}
```

```go
// 修正後
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
}
```


## 検出例

```go
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)

}
```


## 参照
- [net/http - NewRequest](https://golang.org/pkg/net/http/#NewRequest)
- [net/http - NewRequestWithContext](https://golang.org/pkg/net/http/#NewRequestWithContext)
- [net/http - Request.WithContext](https://golang.org/pkg/net/http/#Request.WithContext)