Go Geocoding チュートリアル:Goroutines、Bounded Concurrency、Backoff

本番向けの Go geocoding チュートリアル:goroutine pool、semaphore-bounded concurrency、429 への exponential backoff。コンパイルして動きます。

| May 05, 2026
Go Geocoding チュートリアル:Goroutines、Bounded Concurrency、Backoff

Go は、async/await の儀式なしに concurrency が欲しいときに手を伸ばすべき言語です。Goroutines は 2 KB のスタック、channels は型付きパイプ、そして標準ライブラリの net/http は最初の日からプロダクションで十分使えます。学ぶべき event loop はなく、すべての関数に振りまく await キーワードもなく、coloured-function 問題もありません。直線的なコードを書き、go と semaphore channel で展開します。

これは REST API 経由でアドレスを geocoding する実動 Go チュートリアルです。すべての snippet は .go ファイルに書き、go build を通し、公開前にクリーンな Go 1.22 インストール上で実 API に対して実行しました。SDK もサードパーティ HTTP ライブラリも framework もなく — net/httpencoding/jsonencoding/csvcontext だけです。終わりまでに、任意のサイズの CSV をストリーミングし、設定可能な goroutine pool で行を geocoding し、Retry-After を尊重する jittered exponential backoff で 429 と 5xx を retry し、入力ファイルをメモリに保持せずに {lat, lng, error} CSV を書くプログラムができます。約 80 行の Go です。

全体で使う endpoint は csv2geo.com/api/v1 です。free tier は 1 日 1,000 forward/reverse リクエストに加えて 1 日 100 batch 行で、クレジットカード不要です。サインインして /api-keys からキーを取得して進めてください。

Endpoint

ほぼすべての実世界の geocoding ジョブを 2 つの endpoint がカバーします。Forward はアドレス文字列を座標に変換します。Reverse は座標をアドレスに変換します。両方とも GET (single) または POST (batch) を受け付けます。

# Forward (single)
GET https://csv2geo.com/api/v1/geocode?q=ADDRESS&country=US

# Reverse (single)
GET https://csv2geo.com/api/v1/reverse?lat=LAT&lng=LNG

# Batch forward
POST https://csv2geo.com/api/v1/geocode
Body: { "addresses": ["addr1", "addr2", ...] }

# Auth: either ?api_key=KEY query string, or
# Authorization: Bearer KEY header

単一の forward リクエストのレスポンス形は次のようになります。これはドキュメント例ではなく実際の出力です。

{
  "query": "1600 Pennsylvania Ave NW Washington DC",
  "results": [
    {
      "formatted_address": "1600 Pennsylvania Ave NW, Washington, DC 20500-0005, United States",
      "location": { "lat": 38.89768, "lng": -77.03655 },
      "accuracy": "houseNumber",
      "accuracy_score": 1,
      "components": {
        "house_number": "1600",
        "street": "Pennsylvania Ave NW",
        "city": "Washington",
        "state": "District of Columbia",
        "postal_code": "20500-0005",
        "country": "USA"
      }
    }
  ],
  "meta": { "response_time_ms": 673, "source": "here" }
}

もっとも重要なのは 2 つのフィールドです。results[0].location はあなたの {lat, lng} です。results[0].accuracy はマッチレベルで — "houseNumber" は rooftop、"street" は通り中心、"place" は POI マッチ、"postcode" は postcode 中心です。数値の accuracy_score(0.0–1.0)はより細かい threshold を与えます; どれを選ぶかは geocoding confidence scores explained を参照してください。

最初のリクエスト

net/httpencoding/json だけで十分です。最初のバージョンは座標だけを返すので、wire format をエンドツーエンドで検証できます。

// geocode.go
package main

import (
    "encoding/json"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "time"
)

const baseURL = "https://csv2geo.com/api/v1"

type Location struct {
    Lat float64 `json:"lat"`
    Lng float64 `json:"lng"`
}

type GeocodeResult struct {
    FormattedAddress string   `json:"formatted_address"`
    Location         Location `json:"location"`
    Accuracy         string   `json:"accuracy"`
    AccuracyScore    float64  `json:"accuracy_score"`
}

type GeocodeResponse struct {
    Query   string          `json:"query"`
    Results []GeocodeResult `json:"results"`
}

var client = &http.Client{Timeout: 10 * time.Second}

func geocode(address, country string) (*Location, error) {
    q := url.Values{}
    q.Set("q", address)
    if country != "" {
        q.Set("country", country)
    }
    req, err := http.NewRequest("GET", baseURL+"/geocode?"+q.Encode(), nil)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("CSV2GEO_KEY"))

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 300 {
        return nil, fmt.Errorf("http %d", resp.StatusCode)
    }

    var out GeocodeResponse
    if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
        return nil, err
    }
    if len(out.Results) == 0 {
        return nil, nil
    }
    return &out.Results[0].Location, nil
}

func main() {
    loc, err := geocode("1 Apple Park Way, Cupertino, CA", "US")
    if err != nil {
        fmt.Fprintln(os.Stderr, "error:", err)
        os.Exit(1)
    }
    fmt.Printf("%+v\n", loc)
}

実行:

CSV2GEO_KEY=geo_live_xxx go run geocode.go
# &{Lat:37.33177 Lng:-122.03042}

指摘すべき点がいくつかあります。defer resp.Body.Close() はオプションではありません — 忘れると、底層の TCP 接続がプールに戻されず、負荷下で file descriptor をリークします。直接 http.Client を使うのではなく単一の package-level の http.Get を使ってください; default クライアントは Timeout: 0 で、ハングしたサーバーが goroutine を永遠にブロックする可能性があります。キーが shell history や log ファイルに残らないように、Authorization: Bearer を query 形式の ?api_key= の代わりに使ってください。

Reverse geocoding

同じ形、異なるパラメータ。latlng を渡すと、アドレスが返ってきます。

type ReverseResult struct {
    FormattedAddress string   `json:"formatted_address"`
    Location         Location `json:"location"`
    Accuracy         string   `json:"accuracy"`
    DistanceMeters   float64  `json:"distance_meters,omitempty"`
}

type ReverseResponse struct {
    Results []ReverseResult `json:"results"`
}

func reverse(lat, lng float64) (*ReverseResult, error) {
    q := url.Values{}
    q.Set("lat", fmt.Sprintf("%f", lat))
    q.Set("lng", fmt.Sprintf("%f", lng))

    req, err := http.NewRequest("GET", baseURL+"/reverse?"+q.Encode(), nil)
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("CSV2GEO_KEY"))

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 300 {
        return nil, fmt.Errorf("http %d", resp.StatusCode)
    }

    var out ReverseResponse
    if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
        return nil, err
    }
    if len(out.Results) == 0 {
        return nil, nil
    }
    return &out.Results[0], nil
}

Reverse レスポンスには distance_meters フィールドが含まれます — 一致したアドレスが入力座標からどれだけ離れているかです。delivery アプリの GPS pings を reverse-geocoding する場合、~50m を超えるものは通常 GPS の fix が悪かったことを意味し、geocoder のせいではありません。~10m 未満なら rooftop を見ています。Reverse geocoding における唯一の正直な精度メトリックは ground-truth distance であり、それ以外はすべて少し嘘をつきます。

Go 流の bounded concurrency

Go で 10,000 アドレスを geocoding する間違った方法は for _, a := range addrs { go geocode(a) } です。これは 10,000 の unbounded goroutines を発射し、10,000 の TCP 接続を開き、2 回目の反復で API rate limit を使い切り、同時にローカルの file descriptor も使い切ります。Goroutines は安いですが、無料ではなく、ネットワークが無料になることもありません。

正しい方法は semaphore channel による bounded concurrency です。容量 struct{}N 上限の buffered channel は Go の正典の semaphore です — そこに送信すると、すでに N 個の goroutines が in-flight なら blocking します。

// pool.go
package main

import (
    "fmt"
    "sync"
)

func geocodeMany(addresses []string, concurrency int) []*Location {
    sem := make(chan struct{}, concurrency)
    out := make([]*Location, len(addresses))
    var wg sync.WaitGroup

    for i, addr := range addresses {
        wg.Add(1)
        sem <- struct{}{} // acquire
        go func(i int, addr string) {
            defer wg.Done()
            defer func() { <-sem }() // release

            loc, err := geocode(addr, "US")
            if err != nil {
                fmt.Println("err:", addr, err)
                return
            }
            out[i] = loc
        }(i, addr)
    }
    wg.Wait()
    return out
}

このコードの 3 つのイディオムが場所を勝ち取っています。第一に、sem <- struct{}{}go キーワードの *前* に発生するので、for ループは保留中の数千の goroutines をスケジュールするのではなく、throttle でブロックします。第二に、defer func() { <-sem }() は worker が panic した場合でも slot を解放します — 決してスキップしてはならない defer のチェーンです。第三に、out[i] = loc は goroutine ごとに *一意* の index に書き込むので、mutex は不要です; Go では slice の異なる slot への concurrent write は安全です。goroutine リテラルのパラメータとして iaddr をキャプチャすることで、1.22 以前のすべての Go 開発者を噛んだ loop-variable バグを回避できます。

concurrency の有用な開始ルール: concurrency あなたのプランの per-minute レート制限を取り、60 で割り、おおよそその数の in-flight リクエストを目指します。Free は 100/min — 4 から始めてください。Starter ($49, 1K/min) — 16。Growth ($149, 5K/min) — 64。Pro ($499, 10K/min) — 128 で、ヘッダーを監視します。詳細な内訳は concurrency tuning for geocoding にあります。

Retry、backoff、context パターン

すべての production geocoder が必要とする 3 つのこと: (1) 429 と 5xx を retriable として扱う、(2) サーバーが送ってきたら Retry-After ヘッダーを尊重する、(3) 永久に死んだキーが永遠にループしないように試行回数を制限する。長時間の retry チェーンを caller がキャンセルできるよう context.Context を追加すれば、実サービスに投入できる安全な関数になります。

// retry.go
package main

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "math/rand"
    "net/http"
    "net/url"
    "os"
    "strconv"
    "time"
)

type GeocodeError struct {
    Status  int
    Message string
}

func (e *GeocodeError) Error() string {
    return fmt.Sprintf("geocode: status=%d msg=%s", e.Status, e.Message)
}

func geocodeWithRetry(ctx context.Context, address, country string, maxAttempts int) (*GeocodeResponse, error) {
    q := url.Values{}
    q.Set("q", address)
    if country != "" {
        q.Set("country", country)
    }
    target := baseURL + "/geocode?" + q.Encode()

    var lastErr error
    for attempt := 1; attempt <= maxAttempts; attempt++ {
        req, err := http.NewRequestWithContext(ctx, "GET", target, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+os.Getenv("CSV2GEO_KEY"))

        resp, err := client.Do(req)
        if err != nil {
            // Network errors are retriable.
            lastErr = err
            if attempt == maxAttempts {
                return nil, fmt.Errorf("network error after %d attempts: %w", attempt, err)
            }
            if err := sleep(ctx, backoff(attempt)); err != nil {
                return nil, err
            }
            continue
        }

        if resp.StatusCode < 300 {
            defer resp.Body.Close()
            var out GeocodeResponse
            if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
                return nil, err
            }
            return &out, nil
        }

        // Non-retriable: 4xx other than 429 means bad key, bad input, etc.
        if resp.StatusCode != 429 && resp.StatusCode < 500 {
            body, _ := io.ReadAll(resp.Body)
            resp.Body.Close()
            return nil, &GeocodeError{Status: resp.StatusCode, Message: string(body)}
        }

        // Retriable: 429 or 5xx. Honour Retry-After if present.
        retryAfter := resp.Header.Get("Retry-After")
        resp.Body.Close()
        if attempt == maxAttempts {
            return nil, &GeocodeError{Status: resp.StatusCode, Message: "exhausted retries"}
        }
        delay := backoff(attempt)
        if retryAfter != "" {
            if secs, err := strconv.ParseFloat(retryAfter, 64); err == nil {
                delay = time.Duration(secs * float64(time.Second))
            }
        }
        if err := sleep(ctx, delay); err != nil {
            return nil, err
        }
    }
    return nil, fmt.Errorf("unreachable: lastErr=%v", lastErr)
}

func backoff(attempt int) time.Duration {
    // 1, 2, 4, 8, 16... seconds + up to 1s jitter.
    base := time.Duration(1<<(attempt-1)) * time.Second
    jitter := time.Duration(rand.Float64() * float64(time.Second))
    return base + jitter
}

func sleep(ctx context.Context, d time.Duration) error {
    t := time.NewTimer(d)
    defer t.Stop()
    select {
    case <-ctx.Done():
        return ctx.Err()
    case <-t.C:
        return nil
    }
}

設計についていくつかのメモ。time.NewTimerselect 上の ctx.Done() による select は cancellable sleep を実装する正典の方法です — time.Sleep は context を無視し、deploy や SIGTERM を通じて goroutine を固定したままにします。backoff の jitter は、多くの workers が同時に retry するときに重要です; なければ各 worker が同じ瞬間に目を覚まし、サーバーを re-DDoS します。retry budgets と dead-letter queues の数学については exponential backoff: when to retry, when to stop を参照してください。

成功したレスポンスごとに 3 つの rate-limit ヘッダーが返ってきて、production でチェックする価値があります:

| Header | 意味 | |---|---| | X-RateLimit-Limit | あなたのプランの per-minute 上限 | | X-RateLimit-Remaining | このウィンドウで残っているもの | | X-RateLimit-Reset | ウィンドウがリセットされるまでの Unix 秒 | | Retry-After | 429s で送信される — retry 前に待つ秒数 |

もし RemainingLimit の 10% 未満に落ちたら、自発的にスローダウンしてください — sleep、semaphore を下げる、batch に切り替える。429 で叩きまくるより安いです。これらのヘッダーを生成する limiter algorithm のより深い理論は token bucket vs leaky bucket vs sliding window にあります。

Batch endpoint

Starter ($49/月) 以上のサブスクリプション tier では、1 つの POST で配列のアドレスを取る batch endpoint が利用できます。プラン上限:

| プラン | 月間行数 | Per-minute | Batch サイズ | |---|---|---|---| | Free | — (1K/日 API、100/日 batch) | 100 | 100 | | Starter ($49) | 50,000 | 1,000 | 1,000 | | Growth ($149) | 250,000 | 5,000 | 5,000 | | Pro ($499) | 1,000,000 | 10,000 | 10,000 |

// batch.go
package main

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "os"
    "time"
)

type BatchRequest struct {
    Addresses []string `json:"addresses"`
}

type BatchResponse struct {
    Results []GeocodeResponse `json:"results"`
}

var batchClient = &http.Client{Timeout: 60 * time.Second}

func batchGeocode(ctx context.Context, addresses []string) (*BatchResponse, error) {
    body, err := json.Marshal(BatchRequest{Addresses: addresses})
    if err != nil {
        return nil, err
    }

    req, err := http.NewRequestWithContext(ctx, "POST", baseURL+"/geocode", bytes.NewReader(body))
    if err != nil {
        return nil, err
    }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("CSV2GEO_KEY"))
    req.Header.Set("Content-Type", "application/json")

    resp, err := batchClient.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 300 {
        return nil, fmt.Errorf("batch http %d", resp.StatusCode)
    }

    var out BatchResponse
    if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
        return nil, err
    }
    return &out, nil
}

Batch レスポンスは入力順を保持します: レスポンスの位置 N は常に入力の位置 N に対応します。id フィールドを往復させる必要はありません。専用の batchClient に 60 秒 timeout が設定されていることに注意してください — Growth での 1,000 アドレスの batch はエンドツーエンドで 10–15 秒かかり、singles に使う 10 秒の per-request クライアントよりはるかに長いです。

OOM なしの Streaming CSV

入力ファイルが 100,000 行なら、[][]string に読み込んで range で反復したくはありません。ストリーミングしてください。パターンは worker pool: 1 つの goroutine がディスクから行を読み jobs channel に push し、N 個の worker goroutines が channel を消費して結果を出力 channel に書き、1 つの writer goroutine が出力 channel をディスクに drain します。

// stream.go
package main

import (
    "context"
    "encoding/csv"
    "fmt"
    "io"
    "os"
    "sync"
)

type job struct {
    ID      string
    Address string
}

type result struct {
    ID      string
    Address string
    Lat     float64
    Lng     float64
    Err     string
}

const concurrency = 8

func streamGeocode(ctx context.Context, inPath, outPath string) error {
    in, err := os.Open(inPath)
    if err != nil {
        return err
    }
    defer in.Close()

    out, err := os.Create(outPath)
    if err != nil {
        return err
    }
    defer out.Close()

    reader := csv.NewReader(in)
    writer := csv.NewWriter(out)
    defer writer.Flush()

    header, err := reader.Read()
    if err != nil {
        return err
    }
    idIdx, addrIdx := indexOf(header, "id"), indexOf(header, "address")
    if idIdx < 0 || addrIdx < 0 {
        return fmt.Errorf("csv must have id,address columns")
    }
    if err := writer.Write([]string{"id", "address", "lat", "lng", "error"}); err != nil {
        return err
    }

    jobs := make(chan job, concurrency*2)
    results := make(chan result, concurrency*2)

    var wg sync.WaitGroup
    for i := 0; i < concurrency; i++ {
        wg.Add(1)
        go worker(ctx, &wg, jobs, results)
    }

    // Writer goroutine drains results to disk.
    done := make(chan struct{})
    go func() {
        for r := range results {
            _ = writer.Write([]string{
                r.ID, r.Address,
                fmt.Sprintf("%f", r.Lat), fmt.Sprintf("%f", r.Lng), r.Err,
            })
        }
        close(done)
    }()

    // Producer: read CSV row by row.
    for {
        row, err := reader.Read()
        if err == io.EOF {
            break
        }
        if err != nil {
            close(jobs)
            wg.Wait()
            close(results)
            <-done
            return err
        }
        jobs <- job{ID: row[idIdx], Address: row[addrIdx]}
    }
    close(jobs)
    wg.Wait()
    close(results)
    <-done
    return nil
}

func worker(ctx context.Context, wg *sync.WaitGroup, jobs <-chan job, results chan<- result) {
    defer wg.Done()
    for j := range jobs {
        out, err := geocodeWithRetry(ctx, j.Address, "US", 5)
        if err != nil {
            results <- result{ID: j.ID, Address: j.Address, Err: err.Error()}
            continue
        }
        if len(out.Results) == 0 {
            results <- result{ID: j.ID, Address: j.Address, Err: "no_match"}
            continue
        }
        top := out.Results[0]
        if top.AccuracyScore < 0.7 {
            results <- result{ID: j.ID, Address: j.Address, Err: "low_confidence"}
            continue
        }
        results <- result{
            ID: j.ID, Address: j.Address,
            Lat: top.Location.Lat, Lng: top.Location.Lng,
        }
    }
}

func indexOf(row []string, name string) int {
    for i, c := range row {
        if c == name {
            return i
        }
    }
    return -1
}

func main() {
    if len(os.Args) < 3 {
        fmt.Fprintln(os.Stderr, "usage: stream input.csv output.csv")
        os.Exit(1)
    }
    if err := streamGeocode(context.Background(), os.Args[1], os.Args[2]); err != nil {
        fmt.Fprintln(os.Stderr, "error:", err)
        os.Exit(1)
    }
}

このスクリプトが初心者のコードと違う 3 つの点。第一に、入力は行単位で読まれ、決して全体としてロードされません。第二に、channel バッファは小さく (concurrency*2)、producer は workers に自然に backpressure をかけます — すべての workers が忙しい場合、producer は channel send でブロックし、OS はファイルをほとんどディスク上に保ちます。第三に、エラーは job 全体をクラッシュさせるのではなく、列として記録されます。これが、100K 行の run を完走するスクリプトと、どこで止まったかと聞かれて再起動するスクリプトの違いです。

型付きレスポンス

この投稿の最初にある struct は最小限実行可能な型付けです。オプショナルフィールドで完全カバレッジが欲しいなら、omitempty が友達です — フィールドが存在しないとき JSON 出力をきれいに保ち、レスポンスのデコード時に zero-value の混乱を回避します。

type Components struct {
    HouseNumber string `json:"house_number,omitempty"`
    Street      string `json:"street,omitempty"`
    City        string `json:"city,omitempty"`
    State       string `json:"state,omitempty"`
    PostalCode  string `json:"postal_code,omitempty"`
    Country     string `json:"country,omitempty"`
}

type Meta struct {
    ResponseTimeMs int    `json:"response_time_ms"`
    Source         string `json:"source"`
}

type FullGeocodeResult struct {
    FormattedAddress string     `json:"formatted_address"`
    Location         Location   `json:"location"`
    Accuracy         string     `json:"accuracy"`
    AccuracyScore    float64    `json:"accuracy_score"`
    Components       Components `json:"components"`
}

type FullGeocodeResponse struct {
    Query   string              `json:"query"`
    Results []FullGeocodeResult `json:"results"`
    Meta    Meta                `json:"meta"`
}

Accuracy を型付きエイリアス (type Accuracy string 各値の const 宣言を加えて) として定義すると、switch ステートメントの無料の domain check が得られます。コンパイラは文字列リテラルのタイポを捕まえませんが、Accuracy で switch すると、未処理の値は少なくとも diff で見えます。

Go 開発者を噛むこと

defer resp.Body.Close() を忘れること。 non-nil response を返すすべての http.Client.Do の後には *必ず* body の Close が続かなければなりません。body を一度も読まない場合でもです。Go ランタイムは body が close され drain されるまで底層の TCP 接続を connection pool に戻しません。一度スキップすると、kernel が新しい sockets を拒否するまで、負荷下のサービスは file descriptor をリークします。

デフォルトの http.Client を使うこと。 http.Get 系は http.DefaultClient を使い、それは Timeout: 0 を持ちます。お行儀の悪いサーバーは goroutine を無期限にブロック状態にする可能性があります。明示的な timeout を持つ package-level クライアントを常に宣言するか、deadline 付きで http.NewRequestWithContext を使ってください。同じことが p99 latency story にも当てはまります — timeout がなければ p99 もなく、無限に走るテールがあります。

バッファなし channel デッドロック。 worker が results から読み、writer goroutine が始まっていない場合、send はブロックします。producer が jobs から読み、worker が生きていない場合、send はブロックします。streaming 例の concurrency*2 バッファは forward progress に十分です; 入力全体を保存せず producer が workers より ~1 ステップ先を行ける buffer を選んでください。

ナイーブな WaitGroup + channel パターン。 一般的なバグ: workers が終わる前に results channel を閉じてしまい、writer goroutine が閉じられた channel を見て partial output で終了する。修正は streamGeocode の最後の明示的な順序付けです: close(jobs) を最初に (workers は閉じられた jobs channel を見て退出)、次に wg.Wait() ですべての workers が完了したことを確認、次に close(results)、次に <-done で writer を待ちます。この順序を間違えると、長い入力でしか再現しない missing-rows バグが見えます。

よくある質問

SDK は必要ですか?

いいえ。標準ライブラリ — net/httpencoding/jsoncontexttime — でこの投稿のすべてをカバーします。多くのチームが結局書く SDK は geocodeWithRetry の薄いラッパー、Pydantic 相当の struct セット、worker pool ヘルパーです。サードパーティのリリースケイデンスを追跡し、6 か月ごとに vendored breaking change を扱うより、自分の ~150 行のコードを保守する方が安いです。

エラーを値として扱うべきですか?

はい — それが慣用的な Go です。retry 関数の *GeocodeError 型は HTTP ステータスを型付きフィールドとして運ぶので、caller は文字列をパースせずに switch できます。ネットワークエラーは fmt.Errorf("...: %w", err) でラップして、errors.Iserrors.As が call stack を通して機能し続けるようにしてください。API 境界を越えるものに対しては panic を避けてください; panic は本物のプログラマのバグのために予約してください。

net/httpfasthttp

geocoding クライアントには net/http のままでいてください。fasthttp はマイクロベンチマークレベルで速いですが、API が異なり、HTTP/2 をサポートせず、worker pool に渡したときに噛む方法で goroutines 間で request/response オブジェクトを再利用します。geocoding パイプラインのボトルネックはネットワーク往復であり、HTTP パーサーではありません — fasthttp は API が数百ミリ秒かかる間にナノ秒を節約してくれます。

concurrency をどうチューニングしますか?

あなたのプランの per-minute レート制限を取り、60 で割り、おおよそその数の concurrent リクエストを目指します。1,000/min ÷ 60 ≈ 17 なので、Starter での pool サイズ 16 は安全なデフォルトです。真実のソースは X-RateLimit-Remaining ヘッダーです — 成功した各レスポンスでログに記録し、リクエストあたり 1 より速く落ちないか監視してください。最適な concurrency は曲線で、エルボーは通常 (rate_limit / 60) * 1.5 付近です。完全な方法論は concurrency tuning for geocoding にあります。

no-match と成功した低 confidence マッチをどう区別しますか?

results は no-match では空のスライスです。低 confidence マッチでは results[0] は存在しますが accuracy"postcode" または "place" で、"houseNumber""street" ではなく、accuracy_score は 1.0 を大きく下回ります。妥当なデフォルト threshold は 0.7 です; それ以下は no-match として扱ってください。より厳しいパイプライン (保険リスクスコアリング、ヘルスケア患者マッピング) は 0.95 を使い、accuracy == "houseNumber" を要求します。全体像は geocoding confidence scores explained にあります。

これは US 以外のアドレスでも機能しますか?

はい。country パラメータに正しい ISO alpha-2 を渡してください — ドイツは DE、英国は GB、ブラジルは BR、日本は JP です。カバレッジは今日 39 か国に及び、アドレス数のフルトップ 10 を含みます: USA (1.21 億アドレス)、ブラジル (9000 万)、メキシコ (3000 万)、フランス (2600 万)、イタリア (2600 万)、その他。API ページ に国別カウントが記載されています。

single リクエストと batch endpoint のどちらを使うべきですか?

100 アドレス以上を一度に処理し、プランが許可する場合 (Starter+) は batch を使ってください。1 つの POST は 100 GET よりも双方にとって安く、レイテンシ節約はおおよそ avg_per_request_ms * count / concurrency です。Singles はアドレスが時間とともに到着するストリーミングワークロードでよりシンプルで、free tier では singles のみが選択肢です。完全なトレードオフ分析は batch vs realtime geocoding にあります。

ここからどこへ

API の完全なリファレンスは csv2geo.com/api にあります。別の言語で作業したいなら、Node.js geocoding tutorialfetchp-limit で同じ構造に従い、Python geocoding tutorialhttpxasyncio をカバーします。この投稿が触れるパターンへのより深い掘り下げは: rate limiting algorithms comparedconcurrency tuning for geocoding、そして なぜ p99 latency が平均より重要なのか を参照してください。

エッジケースを見つけたり、この投稿がカバーしていないレスポンス形に当たったり、構築中の Go パイプラインへのフィードバックが欲しい場合、サイトの問い合わせフォームは読んでくれる人に届きます。curl (または go run) の再現を含むバグレポートはすぐに修正されます。

I.A. / CSV2GEO Creator

関連記事

Ready to geocode your addresses?

Use our batch geocoding tool to convert thousands of addresses to coordinates in minutes. Start with 100 free addresses.

Try Batch Geocoding Free →