Go Geocoding 教程:Goroutines、有界并发与 Backoff
生产可用的 Go geocoding 教程:goroutine 池、semaphore 限制并发、对 429 的 exponential backoff。可编译可运行。
当你想要 concurrency 而不想要 async/await 仪式时,Go 是首选语言。Goroutines 是 2 KB 的 stack,channels 是带类型的 pipe,标准库 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/http、encoding/json、encoding/csv 和 context。最后你将得到一个程序,它能 streaming 任意大小的 CSV,用可配置的 goroutine pool 对行做 geocoding,对 429 和 5xx 用尊重 Retry-After 的 jittered exponential backoff 重试,并写出一份 {lat, lng, error} 的 CSV,全程不把输入文件放进内存。大约 80 行 Go。
全文使用的 endpoint 是 csv2geo.com/api/v1。free tier 是每天 1,000 次 forward/reverse 请求外加每天 100 行 batch,无需信用卡。登录并到 /api-keys 取一个 key 跟着做。
Endpoint
两个 endpoint 几乎覆盖了所有现实世界的 geocoding 工作。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" }
}两个字段最重要。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/http 加上 encoding/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 连接无法被归还到 pool,在压力下会泄漏 file descriptor。使用单一的 package-level http.Client 而不是直接用 http.Get;默认客户端的 Timeout: 0 意味着挂死的服务器会永远阻塞你的 goroutine。使用 Authorization: Bearer 而不是 query 形式的 ?api_key=,这样 key 永远不会出现在 shell 历史或 log 文件里。
Reverse geocoding
形状相同,参数不同。传 lat 和 lng,得到一个地址。
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 app 的 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 个无界 goroutines,打开 10,000 个 TCP 连接,在第二轮迭代就用尽 API rate limit,并同时耗尽你本地的 file descriptor。Goroutines 便宜,但不是免费的,网络也从来不免费。
正确的方式是用 semaphore channel 做 bounded concurrency。容量为 struct{}、上限 N 的 buffered channel 是 Go 的标准 semaphore — 一旦已经有 N 个 goroutines 在飞,向其发送就会阻塞。
// 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
}这段代码里有三个习语值得保留。第一,sem <- struct{}{} 发生在 go 关键字 *之前*,因此 for 循环在 throttle 处阻塞,而不是调度成千上万个待执行的 goroutines。第二,defer func() { <-sem }() 即使 worker panic 也会释放 slot — 这条 defer 链你永远不应跳过。第三,out[i] = loc 写入每个 goroutine *唯一* 的 index,因此不需要 mutex;在 Go 中对 slice 不同 slot 的并发写是安全的。把 i 和 addr 作为 goroutine literal 的参数捕获,可避免 1.22 之前咬过每个 Go 开发者的 loop-variable bug。
对 concurrency 一个有用的起步规则:取你计划的 per-minute rate limit,除以 60,瞄准大致那么多的 in-flight 请求。Free 是 100/分 — 从 4 开始。Starter ($49, 1K/分) — 16。Growth ($149, 5K/分) — 64。Pro ($499, 10K/分) — 128 并观察 headers。完整拆解见 concurrency tuning for geocoding。
Retry、backoff 和 context 模式
每个生产级 geocoder 都需要的三件事:(1) 把 429 和 5xx 视为可 retry,(2) 当服务器发送时尊重 Retry-After header,(3) 限制尝试次数,让一个永久死亡的 key 不会永远循环。加上 context.Context,这样 caller 可以取消一条长 retry 链,你就有了一个可以扔进真实服务的安全函数。
// 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.NewTimer 加一个对 select 的 ctx.Done() 上的 select 是实现可取消 sleep 的标准方式 — time.Sleep 忽略 context,会让你的 goroutine 在 deploy 或 SIGTERM 期间一直被钉住。backoff 中的 jitter 在很多 workers 同时 retry 时很重要;没有它,每个 worker 在同一时刻醒来并对服务器再次 DDoS。retry 预算和 dead-letter queues 的数学见 exponential backoff: when to retry, when to stop。
每次成功响应都会返回三个 rate-limit headers,在生产中值得检查:
| Header | 含义 | |---|---| | X-RateLimit-Limit | 你计划的 per-minute 上限 | | X-RateLimit-Remaining | 当前窗口剩余多少 | | X-RateLimit-Reset | 距离窗口重置的 Unix 秒数 | | Retry-After | 在 429s 时发送 — retry 前要等的秒数 |
如果 Remaining 跌到 Limit 的 10% 以下,自愿减速 — sleep、降低 semaphore、切换到 batch。比在 429 上死磕更便宜。这些 headers 由哪种 limiter algorithm 产生的更深理论见 token bucket vs leaky bucket vs sliding window。
Batch endpoint
从 Starter ($49/月) 起的订阅 tier 提供一个 batch endpoint,它在一次 POST 中接受一个地址数组。计划上限:
| 计划 | 月行数 | 每分钟 | 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 遍历。把它 stream 起来。模式是 worker pool:一个 goroutine 从磁盘读行并 push 到 jobs channel,N 个 worker goroutines 消费 channel 并把结果写到 output channel,一个 writer goroutine 把 output 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)
}
}这个脚本做了三件初学者代码通常不会做的事。第一,输入是逐行读的,从不整体加载。第二,channel 的缓冲区很小(concurrency*2),所以 producer 自然地对 workers 形成 backpressure — 如果所有 workers 都忙,producer 在 channel 发送上阻塞,OS 让文件大部分留在磁盘上。第三,错误作为一列被记录而不是把整个 job 拖崩。这就是一个跑完 100K 行的脚本和一个必须用「停在哪了」的问题重启的脚本之间的区别。
类型化响应
本文开头的 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()。 每一次返回非 nil response 的 http.Client.Do *必须* 跟上一个对 body 的 Close,即使你从不读 body。Go runtime 在 body 关闭并 drain 之前不会把底层 TCP 连接归还给 connection pool。漏掉一次,你的服务在压力下会泄漏 file descriptor,直到内核拒绝新的 socket。
使用默认的 http.Client。 http.Get 等同伴使用 http.DefaultClient,它有 Timeout: 0。一个行为不端的服务器会让那个 goroutine 无限期阻塞。始终用显式 timeout 声明一个 package-level 客户端,或者用带 deadline 的 http.NewRequestWithContext。同样的话也适用于 p99 latency 故事 — 没有 timeout 你就没有 p99,你只有一条无限延伸的尾巴。
无缓冲 channel 死锁。 如果 worker 从 results 读取,而 writer goroutine 还没启动,发送就会阻塞。如果 producer 从 jobs 读取,而没有 worker 存活,发送也会阻塞。streaming 例子里的 concurrency*2 缓冲对 forward progress 已经够用;选择一个让 producer 比 workers 领先约 1 步而又不存储整个输入的缓冲。
幼稚的 WaitGroup + channel 模式。 一个常见 bug:在 workers 完成前关闭 results channel,于是 writer goroutine 看到一个已关闭的 channel 并以部分输出退出。修复是在 streamGeocode 末尾的显式顺序:close(jobs) 先(workers 看到 jobs channel 关闭后退出),然后 wg.Wait() 确认所有 workers 都完成,然后 close(results),然后 <-done 等 writer。把这个顺序搞错,你会看到只在长输入上才能复现的 missing-rows bug。
常见问题
需要 SDK 吗?
不需要。标准库 — net/http、encoding/json、context、time — 涵盖本文的一切。大多数团队最终会写的「SDK」是一个围绕 geocodeWithRetry 的薄包装、一组等价于 Pydantic 的 struct 以及一个 worker pool helper。维护你自己的约 150 行代码比跟踪第三方发布节奏和每六个月一次 vendored 破坏性变更更便宜。
应该把错误当作值来处理吗?
是的 — 这是 idiomatic 的 Go。retry 函数中的 *GeocodeError 类型把 HTTP 状态作为类型化字段携带,这样调用方可以对它做 switch 而不必解析字符串。用 fmt.Errorf("...: %w", err) 包装网络错误,让 errors.Is 和 errors.As 在调用栈中继续工作。对任何跨 API 边界的东西避免使用 panic;把 panic 留给真正的程序员 bug。
net/http 对比 fasthttp?
geocoding 客户端继续用 net/http。fasthttp 在微基准层面更快,但 API 不同,不支持 HTTP/2,并且以会咬人的方式在 goroutines 间复用 request/response 对象 — 当你把它们交给 worker pool 时尤其麻烦。geocoding pipeline 的瓶颈是网络往返,不是 HTTP parser — fasthttp 给你节省的是纳秒,而 API 要花数百毫秒。
如何调优 concurrency?
取你计划的 per-minute rate limit,除以 60,瞄准大致那么多并发请求。1,000/分 ÷ 60 ≈ 17,所以在 Starter 上 pool size 16 是一个安全的默认。真相之源是 X-RateLimit-Remaining header — 在每次成功响应里记录它,留意它是否比每请求 1 个掉得更快。最佳 concurrency 是一条曲线,弯肘通常接近 (rate_limit / 60) * 1.5。完整方法见 concurrency tuning for geocoding。
如何区分 no-match 和成功的低 confidence 匹配?
results 在 no-match 时是空 slice。在低 confidence 匹配时 results[0] 存在但 accuracy 是 "postcode" 或 "place" 而不是 "houseNumber" 或 "street",并且 accuracy_score 远低于 1.0。一个合理的默认 threshold 是 0.7;低于这个就当 no-match 处理。更严格的 pipeline(保险风险评分、医疗患者映射)使用 0.95 并要求 accuracy == "houseNumber"。完整图景见 geocoding confidence scores explained。
这对非美国地址有用吗?
有用。在 country 参数中传入正确的 ISO alpha-2 — 德国是 DE,英国是 GB,巴西是 BR,日本是 JP。覆盖范围今天涵盖 39 个国家,包括按地址数前 10 名:USA(1.21 亿地址)、巴西(9000 万)、墨西哥(3000 万)、法国(2600 万)、意大利(2600 万)等。API 页面 列出了每国数量。
应该用单条请求还是 batch endpoint?
当你一次有 ≥100 个地址要做且计划允许时(Starter+)使用 batch。一次 POST 比 100 次 GET 对双方都更便宜,节省的延迟大约是 avg_per_request_ms * count / concurrency。Singles 在地址随时间到达的 streaming workload 中更简单,singles 也是 free tier 上的唯一选择。完整权衡分析见 batch vs realtime geocoding。
接下来去哪
API 的完整参考在 csv2geo.com/api。如果你更愿意用别的语言,Node.js geocoding 教程 用 fetch 和 p-limit 遵循同样结构,Python geocoding 教程 涵盖 httpx 和 asyncio。本文涉及模式的更深入阅读:rate limiting algorithms compared、concurrency tuning for geocoding,以及 为什么 p99 latency 比平均更重要。
如果你发现一个边缘 case,碰到本文未覆盖的响应形状,或想要对你正在构建的 Go pipeline 的反馈,网站上的联系表会到一个会读它的真人。带 curl(或 go run)复现的 bug 报告会被很快修好。
I.A. / CSV2GEO Creator
相关文章
- Node.js Geocoding API 教程:Concurrency、Retries 与 CSV Streaming
- Python Geocoding API 教程:Async、Retries 与 100K 行 CSV Pipeline
- Geocoding 的 Concurrency Tuning:找到你的 Sweet Spot
- Geocoding Pipeline 的 Rate Limiting:Token Bucket vs Leaky Bucket vs Sliding Window
- Geocoding 中的 p99 Latency:为什么你的平均在撒谎
- 多语言 Geocoding 客户端的 Monorepo 模式
Use our batch geocoding tool to convert thousands of addresses to coordinates in minutes. Start with 100 free addresses.
Try Batch Geocoding Free →