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 は 300/min — 5 から始めてください。API 10 から API 100 ($54–$294) は 1,500/min — 25 から。API 250 ($604) は 1,800/min — 30 から始め、ヘッダーを監視します。詳細な内訳は 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

Free を含むすべての tier で、1 つの POST で配列のアドレスを取る batch endpoint が利用できます。プラン上限:

| プラン | 月間クレジット | Per-minute | Batch サイズ | |---|---|---|---| | Free | 3,000/日 | 300 | 1,000 | | API 10 ($54) | 300,000 | 1,500 | 1,000 | | API 25 ($104) | 750,000 | 1,500 | 1,000 | | API 50 ($174) | 1,500,000 | 1,500 | 1,000 | | API 100 ($294) | 3,000,000 | 1,500 | 1,000 | | API 250 ($604) | 7,500,000 | 1,800 | 1,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 が設定されていることに注意してください — 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,500/min ÷ 60 = 25 なので、有料 API tier での pool サイズ 24 は安全なデフォルトです。真実のソースは 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 アドレス以上を一度に処理する場合は batch を使ってください。Free を含むすべての tier で利用できます。1 つの POST は 100 GET よりも双方にとって安く、レイテンシ節約はおおよそ avg_per_request_ms * count / concurrency です。Singles はアドレスが時間とともに到着するストリーミングワークロードでよりシンプルで、batch は free tier でも 1 日 3,000 コールの範囲内で利用できます。完全なトレードオフ分析は 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

関連記事

More in Developers →
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 →