Google Sheets as a REST API in Go
Fetch, search, and parse Google Sheet data in Go using SheetsAPI with net/http, typed structs, and clean error handling.
4 min read
Go's net/http package and strong type system make it straightforward to consume REST APIs.
SheetsAPI returns plain JSON, so you can decode responses directly into Go structs with
no external dependencies - though the popular resty client is shown as an alternative.
Prerequisites
- Go 1.21+
- A Google Sheet with a header row
- A SheetsAPI account and a
YOUR_USER_KEYkey from the dashboard
1. Basic GET request
package main
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
)
const baseURL = "https://sheetsapi.gkit.mreshank.com/api/spreadsheets"
type Meta struct {
Total int `json:"total"`
Limit int `json:"limit"`
Offset int `json:"offset"`
}
type SheetResponse[T any] struct {
Data []T `json:"data"`
Meta Meta `json:"meta"`
}
type Product struct {
Name string `json:"name"`
Price string `json:"price"`
Category string `json:"category"`
Stock string `json:"stock"`
}
func fetchProducts(limit int) (*SheetResponse[Product], error) {
endpoint := fmt.Sprintf("%s/YOUR_USER_KEY/Products", baseURL)
params := url.Values{}
params.Set("limit", fmt.Sprintf("%d", limit))
params.Set("sort", "name")
resp, err := http.Get(endpoint + "?" + params.Encode())
if err != nil {
return nil, fmt.Errorf("http.Get: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var result SheetResponse[Product]
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("json decode: %w", err)
}
return &result, nil
}
func main() {
result, err := fetchProducts(20)
if err != nil {
panic(err)
}
fmt.Printf("Total: %d\n", result.Meta.Total)
for _, p := range result.Data {
fmt.Printf(" %s - %s\n", p.Name, p.Price)
}
}Replace YOUR_USER_KEY with your actual key.
2. Reusable client with auth support
For private sheets, add a Bearer token header:
package sheets
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type Client struct {
baseURL string
userKey string
apiKey string
httpClient *http.Client
}
type QueryOptions struct {
Limit int
Offset int
Search string // e.g. "category:electronics"
Sort string // e.g. "price" or "-price" for descending
Fields string // comma-separated field names
}
func NewClient(userKey, apiKey string) *Client {
return &Client{
baseURL: "https://sheetsapi.gkit.mreshank.com/api/spreadsheets",
userKey: userKey,
apiKey: apiKey,
httpClient: &http.Client{Timeout: 10 * time.Second},
}
}
func Query[T any](ctx context.Context, c *Client, sheet string, opts QueryOptions) (*SheetResponse[T], error) {
endpoint := fmt.Sprintf("%s/%s/%s", c.baseURL, c.userKey, sheet)
params := url.Values{}
if opts.Limit > 0 {
params.Set("limit", fmt.Sprintf("%d", opts.Limit))
}
if opts.Offset > 0 {
params.Set("offset", fmt.Sprintf("%d", opts.Offset))
}
if opts.Search != "" {
params.Set("search", opts.Search)
}
if opts.Sort != "" {
params.Set("sort", opts.Sort)
}
if opts.Fields != "" {
params.Set("fields", opts.Fields)
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint+"?"+params.Encode(), nil)
if err != nil {
return nil, err
}
if c.apiKey != "" {
req.Header.Set("Authorization", "Bearer "+c.apiKey)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("api error: status %d", resp.StatusCode)
}
var result SheetResponse[T]
return &result, json.NewDecoder(resp.Body).Decode(&result)
}Usage:
client := sheets.NewClient("YOUR_USER_KEY", "sk_your_api_key_here")
products, err := sheets.Query[Product](context.Background(), client, "Products", sheets.QueryOptions{
Limit: 50,
Search: "category:electronics",
Sort: "-price", // descending price
})3. Pagination helper
Collect all rows across multiple pages:
func FetchAll[T any](ctx context.Context, c *Client, sheet string) ([]T, error) {
const pageSize = 100
var all []T
offset := 0
for {
resp, err := Query[T](ctx, c, sheet, QueryOptions{
Limit: pageSize,
Offset: offset,
})
if err != nil {
return nil, err
}
all = append(all, resp.Data...)
offset += pageSize
if offset >= resp.Meta.Total {
break
}
}
return all, nil
}4. Gin HTTP handler
Expose your Sheet data through a Go web service:
package main
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"yourapp/sheets"
)
var client = sheets.NewClient("YOUR_USER_KEY", "sk_your_api_key_here")
type Product struct {
Name string `json:"name"`
Price string `json:"price"`
Category string `json:"category"`
}
func main() {
r := gin.Default()
r.GET("/products", func(c *gin.Context) {
q := c.Query("q")
search := ""
if q != "" {
search = "name:" + q
}
result, err := sheets.Query[Product](context.Background(), client, "Products", sheets.QueryOptions{
Limit: 20,
Search: search,
})
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, result)
})
r.Run(":8080")
}5. In-memory TTL cache
Avoid hammering the API on every request:
import (
"sync"
"time"
)
type cached[T any] struct {
value T
expires time.Time
}
type TTLCache[T any] struct {
mu sync.Mutex
store map[string]cached[T]
ttl time.Duration
}
func NewTTLCache[T any](ttl time.Duration) *TTLCache[T] {
return &TTLCache[T]{store: make(map[string]cached[T]), ttl: ttl}
}
func (c *TTLCache[T]) Get(key string) (T, bool) {
c.mu.Lock()
defer c.mu.Unlock()
entry, ok := c.store[key]
if !ok || time.Now().After(entry.expires) {
var zero T
return zero, false
}
return entry.value, true
}
func (c *TTLCache[T]) Set(key string, value T) {
c.mu.Lock()
defer c.mu.Unlock()
c.store[key] = cached[T]{value: value, expires: time.Now().Add(c.ttl)}
}Wrap your query:
var cache = NewTTLCache[[]Product](5 * time.Minute)
func cachedProducts(ctx context.Context, client *sheets.Client) ([]Product, error) {
if products, ok := cache.Get("products"); ok {
return products, nil
}
result, err := sheets.Query[Product](ctx, client, "Products", sheets.QueryOptions{Limit: 200})
if err != nil {
return nil, err
}
cache.Set("products", result.Data)
return result.Data, nil
}6. Query parameter reference
| Parameter | Example | Description |
|---|---|---|
limit | 50 | Rows per page (default: 20, max: 500) |
offset | 0 | Row offset for pagination |
search | category:books | Filter by field value |
sort | price or -price | Ascending / descending |
fields | name,price | Return only named columns |
Summary
| Concern | Approach |
|---|---|
| HTTP | net/http with context timeout |
| Types | Generic SheetResponse[T] struct |
| Auth | Authorization: Bearer header |
| Pagination | Offset loop until offset >= meta.total |
| Caching | Generic in-memory TTL cache |
No SDK needed - SheetsAPI speaks plain JSON over HTTPS, and Go's standard library handles the rest.