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.
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.
GKit is currently undergoing platform maintenance
Be the first to know when all Google Workspace tools are back online, plus get our engineering deep-dives and product announcements directly to your inbox.