A Go SERP client should map the response envelope, use context cancellation, and keep retries within a visible budget. The client below reads SearchCans data.organic into typed results. It treats a missing rank as missing, rather than manufacturing position zero. This is an implementation tutorial, not navigation for an unrelated GoSerp product.
Implement the request and response contract
Create a package named serp. Set Endpoint to https://www.searchcans.com/api/v1/search, read the key from your environment, and supply &http.Client{Timeout:60*time.Second}. Call Search with a context deadline. Keep credentials out of logs.
package serp
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"time"
)
type Result struct {
Title string `json:"title"`
Link string `json:"link"`
Position *int `json:"position"`
}
type Client struct { Endpoint, Key string; HTTP *http.Client }
func (c Client) Search(ctx context.Context, query string) ([]Result, error) {
payload, err := json.Marshal(map[string]string{"t":"google", "s":query, "country":"us", "language":"en"})
if err != nil { return nil, err }
for attempt := 0; attempt < 2; attempt++ {
req, err := http.NewRequestWithContext(ctx, "POST", c.Endpoint, bytes.NewReader(payload))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer " + c.Key)
req.Header.Set("Content-Type", "application/json")
response, err := c.HTTP.Do(req)
if err != nil { return nil, err }
if response.StatusCode == 429 && attempt == 0 {
response.Body.Close()
seconds, err := strconv.Atoi(response.Header.Get("Retry-After"))
if err != nil || seconds < 0 || seconds > 30 { return nil, fmt.Errorf("429 requires explicit bounded retry policy") }
timer := time.NewTimer(time.Duration(seconds)*time.Second)
select { case <-ctx.Done(): timer.Stop(); return nil, ctx.Err(); case <-timer.C: }
continue
}
if response.StatusCode != 200 { response.Body.Close(); return nil, fmt.Errorf("HTTP %d", response.StatusCode) }
var envelope struct {
Code *int `json:"code"`
Data *struct { Organic *[]Result `json:"organic"` } `json:"data"`
}
err = json.NewDecoder(response.Body).Decode(&envelope)
response.Body.Close()
if err != nil { return nil, err }
if envelope.Code == nil || *envelope.Code != 0 || envelope.Data == nil || envelope.Data.Organic == nil {
return nil, fmt.Errorf("business or schema error")
}
for _, row := range *envelope.Data.Organic { if row.Link == "" { return nil, fmt.Errorf("result link missing") } }
return *envelope.Data.Organic, nil
}
return nil, fmt.Errorf("retry exhausted")
}
Bound concurrency outside the client
The function handles one search. Run it through a fixed worker pool when you have several queries; begin with three workers only if your account has at least three Parallel Lanes. Do not launch one goroutine per URL without a limit. A concurrency allowance is simultaneous work, not a guaranteed requests-per-second rate. Cancellation should stop queued work as well as in-flight requests.
Test failures with an HTTP test server
Use net/http/httptest to serve a valid envelope, an empty organic list, a malformed data object and a missing result link. Test a nonzero business code independently of HTTP status. For HTTP 429, this implementation accepts a numeric Retry-After from zero to thirty seconds and retries once. An absent, date-form or larger value returns an error for a caller policy to handle. No retry is attempted on other HTTP failures.
A cancelled context must end the request or backoff. Do not count an attempt that failed validation as a usable result. Record query, market, status, attempts and elapsed time without recording the key. The matching tests are local contract tests; they do not demonstrate live throughput or vendor reliability.
For what the fields mean, read the SERP API definition. For system-level source and action policy, use the agent architecture. Actual timing comparisons belong in the benchmark method.