🔍

Go: fasthttpはなぜ速い? 内部実装とVegetaベンチマークで徹底検証

に公開

TL;DR

  • fasthttp はp50/p90 レイテンシで優位(p50 で約 15%、p90 で約 27%高速)
  • 一方、mean(平均)や p99(テールレイテンシ)では net/http が優位なケースも
  • 速さの理由: Worker Poolによる Goroutine 再利用、[]byteベースのアロケーション削減設計、sync.Poolによるオブジェクト再利用
  • HTTP/2 非対応などの制約があるため、SLA 要件や負荷パターンに応じて選択が必要

はじめに

Go 言語で HTTP サーバーを実装する際、標準ライブラリのnet/httpを使うのが一般的です。しかし、高負荷環境ではvalyala/fasthttpという選択肢があります。fasthttp は「net/http より最大 10 倍速い」と謳われており、高性能 Web フレームワークであるFiberの基盤としても採用されています。

本記事では、fasthttp がなぜ速いのかを内部実装から解説し、Vegeta による負荷テストで実際の性能差を検証します。

net/http と fasthttp の内部実装の違い

1. Goroutine 生成モデルの違い

net/httpは、接続が確立された時点で新しい Goroutine を生成し、その接続の全リクエストを処理します:

net/http/server.go
// net/http の server.go より簡略化
func (srv *Server) Serve(l net.Listener) error {
    baseCtx := context.Background()
    ctx := context.WithValue(baseCtx, ServerContextKey, s)
    for {
        rw, err := l.Accept()
        if err != nil {
            return err
        }
        connCtx := ctx
    	if cc := s.ConnContext; cc != nil {
            connCtx = cc(connCtx, rw)
            if connCtx == nil {
                panic("ConnContext returned nil")
            }
        }
        tempDelay = 0
        c := s.newConn(rw)
        c.setState(c.rwc, StateNew, runHooks)
        // 接続が確立された時点で新しいGoroutineを生成
        // このGoroutineは接続が閉じられるまで、その接続の全リクエストを処理する
        go c.serve(connCtx)
    }
}

一方、fasthttpは Worker Pool パターンを採用し、Goroutine を再利用します:

fasthttp/workerpool.go
// fasthttp の workerpool.go より簡略化

type workerChan struct {
    lastUseTime time.Time
    ch          chan net.Conn
}

func (wp *workerPool) Serve(c net.Conn) bool {
    // readyキュー(FILO)からアイドル状態のworkerを取得
    ch := wp.getCh()
    if ch == nil {
        return false
    }
    // 既存のworkerに接続を渡す(新規Goroutine生成なし)
    ch.ch <- c
    return true
}

func (wp *workerPool) getCh() *workerChan {
    var ch *workerChan

    wp.lock.Lock()
    ready := wp.ready
    n := len(ready) - 1
    if n >= 0 {
        // FILO: 最後に使われたworkerを優先的に取得
        // → CPUキャッシュの局所性を維持
        ch = ready[n]
        ready[n] = nil
        wp.ready = ready[:n]
    }
    wp.lock.Unlock()

    if ch == nil {
        // プールにworkerがない場合のみ新規作成
        vch := wp.workerChanPool.Get()
        ch = vch.(*workerChan)
        go func() {
            wp.workerFunc(ch)
            wp.workerChanPool.Put(vch) // 処理完了後にプールに返却
        }()
    }
    return ch
}

この違いを図で表すと:

net/http の処理モデル

fasthttp の処理モデル

FILO(First In, Last Out)を採用する理由: 実装上の選択であり、メモリ効率や実装の簡潔さを考慮したものと考えられます。

2. メモリアロケーションの違い

net/httpでは、HTTP ヘッダーはmap[string][]stringにパースされます:

// net/http の Request 構造体
type Request struct {
    Header Header // type Header map[string][]string
    // ...
}

// リクエスト受信時の処理(簡略化)
// 1. []byte でヘッダーを読み取り
// 2. string に変換(メモリアロケーション発生)
// 3. map に格納(メモリアロケーション発生)

fasthttpでは、ヘッダーは[]byteのまま保持し、必要になるまでパースしません(遅延評価):

fasthttp/header.go
// fasthttp の RequestHeader 構造体
// 参照: https://github.com/valyala/fasthttp/blob/master/header.go

// 共通ヘッダー情報を持つ埋め込み構造体
type header struct {
    h               []argsKV  // 汎用ヘッダーをkey-valueペアで格納
    contentLength   int       // Content-Lengthの数値
    contentLengthBytes []byte // Content-Lengthの生バイト列
    contentType     []byte    // Content-Typeの値
    // ...
}

type RequestHeader struct {
    header                   // 共通ヘッダー情報を埋め込み
    noCopy           noCopy  // コピー防止用
    method           []byte  // HTTPメソッド (GET, POST等)
    requestURI       []byte  // リクエストURI
    host             []byte  // Hostヘッダー
    userAgent        []byte  // User-Agentヘッダー
    rawHeaders       []byte  // 受信したヘッダーの生データ
    cookiesCollected bool    // Cookie解析済みフラグ
    // ...
}

設計のポイント: 頻繁に使用されるヘッダー(Host, User-Agent, Content-Type 等)は専用フィールドで高速アクセス。その他のヘッダーは[]argsKVスライスで動的に管理し、rawHeadersに生データを保持して必要時のみパースします。

fasthttp/header.go
// ヘッダーアクセス時(アロケーションなし)
func (h *RequestHeader) ContentType() []byte {
    return h.contentType // []byteを直接返す
}

func (h *RequestHeader) Host() []byte {
    return h.host // stringへの変換なし
}

3. sync.Pool によるオブジェクト再利用

fasthttp は多数のsync.Poolを使用して、頻繁に使用されるオブジェクトを再利用します:

// fasthttp での Request/Response のプール管理
var (
    requestPool = &sync.Pool{
        New: func() interface{} {
            return &Request{}
        },
    }
    responsePool = &sync.Pool{
        New: func() interface{} {
            return &Response{}
        },
    }
)

// オブジェクト取得(プールから再利用 or 新規作成)
func AcquireRequest() *Request {
    return requestPool.Get().(*Request)
}

// オブジェクト返却(リセットしてプールに戻す)
func ReleaseRequest(req *Request) {
    req.Reset()
    requestPool.Put(req)
}

性能差が生まれる仕組みのまとめ

観点 net/http fasthttp
Goroutine 接続確立時に新規作成 Worker Pool で再利用
ヘッダー格納 map[string][]string []byte(遅延パース)
Request/Response 毎回新規作成 sync.Poolで再利用
文字列処理 string変換が多い []byteのまま処理

fasthttp クライアントの実装

基本的な使い方

package main

import (
    "fmt"
    "log"

    "github.com/valyala/fasthttp"
)

func main() {
    // Requestを取得(プールから)
    req := fasthttp.AcquireRequest()
    defer fasthttp.ReleaseRequest(req)

    // Responseを取得(プールから)
    resp := fasthttp.AcquireResponse()
    defer fasthttp.ReleaseResponse(resp)

    // リクエストの設定
    req.SetRequestURI("https://httpbin.org/get")
    req.Header.SetMethod(fasthttp.MethodGet)
    req.Header.Set("User-Agent", "my-app/1.0")

    // リクエスト実行
    if err := fasthttp.Do(req, resp); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Status: %d\n", resp.StatusCode())
    fmt.Printf("Body: %s\n", resp.Body())
}

高性能 HTTP クライアントの設計

本番環境ではfasthttp.Clientを適切に設定します:

package main

import (
    "log"
    "net"
    "time"

    "github.com/valyala/fasthttp"
)

func NewHTTPClient() *fasthttp.Client {
    // TCPDialerの設定
    dialer := &fasthttp.TCPDialer{
        Concurrency:      4096,              // 同時接続試行数
        DNSCacheDuration: 5 * time.Minute,   // DNSキャッシュ有効期間
    }

    return &fasthttp.Client{
        // タイムアウト設定
        ReadTimeout:         30 * time.Second,
        WriteTimeout:        10 * time.Second,
        MaxConnWaitTimeout:  10 * time.Second,

        // 接続プール設定
        MaxConnsPerHost:     1024,            // ホストごとの最大接続数
        MaxIdleConnDuration: 30 * time.Second,

        // リトライ設定(冪等なリクエストのみ)
        MaxIdemponentCallAttempts: 3,

        // パフォーマンスチューニング
        NoDefaultUserAgentHeader:      true,  // デフォルトUser-Agentを送信しない
        DisableHeaderNamesNormalizing: true,  // ヘッダー名の正規化を無効化
        DisablePathNormalizing:        true,  // パスの正規化を無効化

        // カスタムDialer(リトライ機構付き)
        // 注意: Dialを使用する場合、Request timeoutが無視されます。
        // Request timeoutを考慮する場合は、DialTimeoutを使用してください。
        Dial: func(addr string) (net.Conn, error) {
            const maxRetries = 3
            var lastErr error
            for i := 0; i < maxRetries; i++ {
                conn, err := dialer.DialTimeout(addr, 5*time.Second)
                if err == nil {
                    return conn, nil
                }
                lastErr = err
                // タイムアウト以外のエラーはリトライしない
                if netErr, ok := err.(net.Error); !ok || !netErr.Timeout() {
                    return nil, err
                }
            }
            return nil, lastErr
        },
    }
}

各設定項目の解説:

設定項目 説明 推奨値の考え方
Concurrency 同時 Dial 数の上限(0 で無制限) CPU コア数 × 256〜1024
DNSCacheDuration DNS キャッシュの有効期間(デフォルト 1 分) 1〜5 分(変更頻度による)
ReadTimeout レスポンス読み取りタイムアウト(ボディを含む) バックエンドの応答時間 + α
WriteTimeout リクエスト書き込みタイムアウト(ボディを含む) 通常は短め(5〜10 秒)
MaxConnsPerHost ホストごとの最大接続数(デフォルト 512) 想定同時リクエスト数に合わせる
MaxIdleConnDuration アイドル接続の保持時間(デフォルト 10 秒) サーバーの Keep-Alive 設定に合わせる

エラーハンドリング

fasthttp には独自のエラー型があるため、適切にハンドリングが必要です。

import (
    "errors"
    "fmt"
    "net"
    "time"

    "github.com/valyala/fasthttp"
)

func executeRequest(client *fasthttp.Client, req *fasthttp.Request, resp *fasthttp.Response) error {
    err := client.DoTimeout(req, resp, 30*time.Second)
    if err != nil {
        switch {
        case errors.Is(err, fasthttp.ErrTimeout):
            return fmt.Errorf("request timeout")
        case errors.Is(err, fasthttp.ErrNoFreeConns):
            return fmt.Errorf("connection pool exhausted")
        case errors.Is(err, fasthttp.ErrDialTimeout):
            return fmt.Errorf("dial timeout: %w", err)
        case errors.Is(err, fasthttp.ErrConnectionClosed):
            return fmt.Errorf("connection closed by server: %w", err)
        default:
            // net.Error などのネットワークエラー
            var netErr net.Error
            if errors.As(err, &netErr) {
                if netErr.Timeout() {
                    return fmt.Errorf("network timeout: %w", err)
                }
                return fmt.Errorf("network error: %w", err)
            }
            return fmt.Errorf("request failed: %w", err)
        }
    }
    return nil
}

fasthttp サーバーの実装

基本的なサーバー

package main

import (
    "encoding/json"
    "log"
    "time"

    "github.com/valyala/fasthttp"
)

func main() {
    handler := func(ctx *fasthttp.RequestCtx) {
        switch string(ctx.Path()) {
        case "/api/health":
            ctx.SetStatusCode(fasthttp.StatusOK)
            ctx.SetBodyString("OK")

        case "/api/users":
            ctx.SetContentType("application/json")
            response := map[string]interface{}{
                "users": []string{"alice", "bob", "charlie"},
            }
            if err := json.NewEncoder(ctx).Encode(response); err != nil {
                ctx.SetStatusCode(fasthttp.StatusInternalServerError)
                return
            }

        default:
            ctx.SetStatusCode(fasthttp.StatusNotFound)
        }
    }

    server := &fasthttp.Server{
        Handler:            handler,
        MaxConnsPerIP:      1000,
        MaxRequestsPerConn: 1000,
        ReadTimeout:        10 * time.Second,
        WriteTimeout:       10 * time.Second,
        IdleTimeout:        30 * time.Second,
    }

    log.Println("Starting server on :8080")
    if err := server.ListenAndServe(":8080"); err != nil {
        log.Fatal(err)
    }
}

fasthttp/router を使ったルーティング

パスパラメータや HTTP メソッドごとのルーティングが必要な場合は、fasthttp/routerを使うと便利です。

package main

import (
    "encoding/json"
    "log"

    "github.com/fasthttp/router"
    "github.com/valyala/fasthttp"
)

func main() {
    r := router.New()

    // GETリクエスト
    r.GET("/api/health", func(ctx *fasthttp.RequestCtx) {
        ctx.SetStatusCode(fasthttp.StatusOK)
        ctx.SetBodyString("OK")
    })

    // パスパラメータを使用({name}形式)
    r.GET("/api/users/{id}", func(ctx *fasthttp.RequestCtx) {
        userID := ctx.UserValue("id")
        if userID == nil {
            ctx.SetStatusCode(fasthttp.StatusBadRequest)
            return
        }
        ctx.SetContentType("application/json")
        json.NewEncoder(ctx).Encode(map[string]string{
            "id":   userID.(string),
            "name": "example-user",
        })
    })

    // POSTリクエスト
    r.POST("/api/users", func(ctx *fasthttp.RequestCtx) {
        ctx.SetStatusCode(fasthttp.StatusCreated)
        ctx.SetBodyString("Created")
    })

    log.Println("Starting server on :8080")
    if err := fasthttp.ListenAndServe(":8080", r.Handler); err != nil {
        log.Fatal(err)
    }
}

fasthttp/router では以下のパスパターンが使用できます:

パターン 説明
{name} 名前付きパラメータ /users/{id} → id 取得
{name?} オプションパラメータ /users/{id?} → id は省略可
{name:*} キャッチオール /files/{path:*} → 残り全パス
{name:[0-9]+} 正規表現検証 /users/{id:[0-9]+} → 数字のみ

RequestCtx の注意点

非同期処理で参照する場合は、必ず値をコピーしてください:

import (
    "log"
    "time"

    "github.com/valyala/fasthttp"
)

// ❌ 間違い: handlerを抜けた後にRequestCtxを参照
func handler(ctx *fasthttp.RequestCtx) {
    go func() {
        time.Sleep(time.Second)
        // ctx はhandler終了後に再利用される可能性がある
        log.Println(string(ctx.Path())) // 危険!データ競合が発生
    }()
}

// ✅ 正解: 必要な値をコピーしてから使用
func handler(ctx *fasthttp.RequestCtx) {
    path := string(ctx.Path()) // コピー
    userID := string(ctx.QueryArgs().Peek("user_id")) // コピー

    go func() {
        time.Sleep(time.Second)
        log.Printf("path: %s, userID: %s\n", path, userID) // 安全
    }()
}

Vegeta による性能比較

Vegetaは Go 製の負荷テストツールです。ここでは、net/http と fasthttp の性能を実際に比較します。

比較用サーバーの実装

net/http サーバー:

// benchmark/nethttp-server/main.go
package main

import (
    "encoding/json"
    "net/http"
)

func main() {
    http.HandleFunc("/api/test", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(map[string]string{
            "message": "Hello from net/http",
            "server":  "net/http",
        })
    })
    http.ListenAndServe(":8080", nil)
}

fasthttp サーバー:

// benchmark/fasthttp-server/main.go
package main

import (
    "encoding/json"

    "github.com/valyala/fasthttp"
)

func main() {
    handler := func(ctx *fasthttp.RequestCtx) {
        if string(ctx.Path()) == "/api/test" {
            ctx.SetContentType("application/json")
            json.NewEncoder(ctx).Encode(map[string]string{
                "message": "Hello from fasthttp",
                "server":  "fasthttp",
            })
        }
    }
    fasthttp.ListenAndServe(":8081", handler)
}

ベンチマーク結果

以下はローカル環境(macOS, Apple M2, Go 1.24)で計測した結果です。ベンチマークの実行方法は こちらのREADME を参照してください。

1000 req/sec (10 秒間) - 低負荷時

net/http:

Requests      [total, rate, throughput]  10000, 1000.10, 1000.09
Latencies     [min, mean, 50, 90, 95, 99, max]
              63.209µs, 218.438µs, 153.485µs, 256.57µs, 298.281µs, 891.545µs, 12.896ms
Success       [ratio]  100.00%

fasthttp:

Requests      [total, rate, throughput]  10000, 1000.09, 1000.07
Latencies     [min, mean, 50, 90, 95, 99, max]
              56.833µs, 171.587µs, 133.227µs, 212.734µs, 252.216µs, 819.324µs, 17.536ms
Success       [ratio]  100.00%

両者とも 100%成功。平均レイテンシは fasthttp がやや優位(net/http 218µs vs fasthttp 171µs)。

10000 req/sec (10 秒間) - 高負荷時

net/http:

Requests      [total, rate, throughput]  100000, 10000.06, 9999.98
Latencies     [min, mean, 50, 90, 95, 99, max]
              26.292µs, 203.527µs, 44.352µs, 59.967µs, 123.443µs, 3.39ms, 65.117ms
Success       [ratio]  100.00%

fasthttp:

Requests      [total, rate, throughput]  100000, 10000.08, 10000.00
Latencies     [min, mean, 50, 90, 95, 99, max]
              22.875µs, 212.301µs, 38.607µs, 57.783µs, 125.328µs, 2.559ms, 72.081ms
Success       [ratio]  100.00%

差が出るポイント:

  • p50 レイテンシ: net/http 44µs vs fasthttp 38µs(fasthttp が約 13%高速)
  • p99 レイテンシ: net/http 3.39ms vs fasthttp 2.56ms(fasthttp が約 25%高速)
20000 req/sec (10 秒間) - 超高負荷時

net/http:

Requests      [total, rate, throughput]  200000, 19999.99, 19999.64
Latencies     [min, mean, 50, 90, 95, 99, max]
              24.792µs, 131.654µs, 45.249µs, 126.445µs, 197.214µs, 1.175ms, 51.057ms
Success       [ratio]  100.00%

fasthttp:

Requests      [total, rate, throughput]  200000, 20000.05, 19999.83
Latencies     [min, mean, 50, 90, 95, 99, max]
              21.584µs, 237.142µs, 38.119µs, 92.174µs, 187.581µs, 6.184ms, 52.674ms
Success       [ratio]  100.00%

差が出るポイント:

  • p50 レイテンシ: net/http 45µs vs fasthttp 38µs(fasthttp が約 15%高速)
  • p90 レイテンシ: net/http 126µs vs fasthttp 92µs(fasthttp が約 27%高速)

ベンチマーク結果の考察

一部の指標では net/http が優位なケースがあります。

20000 req/sec での詳細比較

指標 net/http fasthttp 優位
min 24.79µs 21.58µs fasthttp
mean 131.65µs 237.14µs net/http
p50 45.25µs 38.12µs fasthttp
p90 126.45µs 92.17µs fasthttp
p99 1.175ms 6.184ms net/http
max 51.06ms 52.67ms net/http

fasthttp は p50 や p90 で優位ですが、mean(平均)や p99(テールレイテンシ)では net/http が優位という結果になっています。

なぜこのような結果になるのか

以下は、ベンチマーク結果から推測される要因です。実際の原因を特定するには、pprof などのプロファイリングツールを使用した詳細な分析が必要です。

1. Worker Pool の拡張コスト

負荷が急激に増加する際の新しい worker 作成による初期化コストが一部のリクエストに影響する可能性があります。

fasthttp/workerpool.go
// fasthttp workerpool.go より(簡略化)
// readyキューが空かつ MaxWorkersCount 未満の場合に新規作成
if ch == nil && wp.workersCount < wp.MaxWorkersCount {
    wp.workersCount++
    ch = wp.workerChanPool.Get().(*workerChan)
    go wp.workerFunc(ch)  // ここで新規Goroutine生成のオーバーヘッド
}

2. sync.Pool の特性

sync.Poolに格納されたアイテムは、通知なく自動的に削除される可能性があります(公式ドキュメント: "Any item stored in the Pool may be removed automatically at any time without notification")。Go の実装では、各 GC cycle の開始時にプール内のオブジェクトがクリアされることが知られています。これは、sync.Poolの設計思想が「一時的なオブジェクトの再利用を最適化し、GC への圧力を軽減する」ことにあり、GC cycle の開始時にプール内のオブジェクトが解放されるためです。

この特性により、以下のような影響が発生します:

  • GC cycle 開始直後: プール内のオブジェクトがクリアされるため、次のリクエストで新規オブジェクトのアロケーションが必要になる可能性が高まる
  • 新規アロケーションのコスト: メモリ確保、初期化処理、GC 圧力の増加が発生し、レイテンシが増加する可能性がある
  • テールレイテンシへの影響: 大多数のリクエストは高速ですが、GC cycle 開始直後のリクエストは新規アロケーションが必要となり、レイテンシが跳ね上がる可能性がある

特に高負荷時には GC の実行頻度が高くなる傾向があるため、この影響がテールレイテンシ(p99)に現れる可能性があります。

3. net/http の均一なオーバーヘッド

net/http は毎回新規 Goroutine を生成するため、オーバーヘッドが比較的均一に分散される傾向があり、テールレイテンシが比較的安定する傾向があります。一方、fasthttp は大多数のリクエストは高速ですが、Worker Pool 拡張や sync.Pool からのオブジェクト削除のタイミングで一部のリクエストが遅くなる可能性があります。

実運用での選択指針

  • p50/p90 を重視する場合: fasthttp が有利(大多数のリクエストが高速)
  • テールレイテンシ(p99)の SLA が厳しい場合: net/http の方が安定する可能性あり
  • 平均レイテンシを重視する場合: 負荷パターンによって異なる

Tips

HTML レポートの生成

Vegeta はテスト結果を HTML グラフとして出力できます。

# 詳細なレポートを生成
vegeta attack -targets=targets_fasthttp.txt -rate=5000 -duration=30s | \
  tee results.bin | \
  vegeta report

# HTMLグラフを生成
vegeta plot results.bin > results.html
pprof によるプロファイリング

メモリアロケーションの差を確認するには、pprof を使用します。

import (
    "net/http"
    _ "net/http/pprof"
)

func main() {
    // pprofエンドポイントを有効化
    go func() {
        http.ListenAndServe(":6060", nil)
    }()

    // メインのサーバー処理...
}
# 負荷をかけながらプロファイル取得
go tool pprof -http=:8082 http://localhost:6060/debug/pprof/heap
go tool pprof -http=:8083 http://localhost:6060/debug/pprof/allocs

fasthttp を使う際の注意点

1. net/http との API 非互換性

fasthttp はnet/httpと同じ API を提供していません:

net/http fasthttp
http.Handler interface fasthttp.RequestHandler function
http.Request *fasthttp.RequestCtx
http.ResponseWriter *fasthttp.RequestCtx
r.URL.Query().Get("key") ctx.QueryArgs().Peek("key")
r.Header.Get("Key") ctx.Request.Header.Peek("Key")
w.Write([]byte) ctx.Write([]byte)

2. HTTP/2 非対応

3. 接続が予期せず閉じられる問題

サーバーが最初のレスポンスバイトを返す前に接続を閉じた場合、ErrConnectionClosedエラーが発生します。対処方法: サーバー側でConnection: closeレスポンスヘッダーを設定するか、クライアント側でリクエストにConnection: closeヘッダーを追加します。詳細は「エラーハンドリング」セクションを参照してください。

4. いつ fasthttp を使うべきか

適している場面:

  • 毎秒数千〜数万リクエストを処理する
  • 一貫した低レイテンシが求められる
  • メモリ使用量を最小限に抑えたい
  • HTTP/1.1 で十分

適していない場面:

  • 標準的な負荷(〜数百 req/sec)
  • 既存の net/http ミドルウェアエコシステムを活用したい
  • HTTP/2、gRPC が必要
  • WebSocket を多用する(fasthttp でも可能だが、gorilla/websocket の方が成熟)

まとめ

fasthttp が net/http より高速な理由は、Worker Pool パターンによる Goroutine 再利用、sync.Poolによるオブジェクト再利用、[]byte ベースのアロケーション削減設計、遅延評価によるメモリ効率化です。

ベンチマーク結果からの知見:

  • fasthttp はp50/p90 レイテンシで優位(大多数のリクエストが高速)
  • ただしmean(平均)や p99(テールレイテンシ)では net/http が優位なケースもある

選択の指針:

  • p50/p90 を重視する場合 → fasthttp
  • テールレイテンシ(p99)の SLA が厳しい場合 → net/http も検討
  • HTTP/2 や gRPC が必要な場合 → net/http

自身のプロジェクトの要件(SLA、負荷パターン、プロトコル、エコシステム)を考慮して選択してください。

参考リンク

公式ドキュメント

参考記事

Discussion