Go connector guide
This guide covers the full lifecycle of building a Go connector, ETL pipeline, or data service on top of ClickHouse using github.com/ClickHouse/clickhouse-go/v2. It is opinionated: it recommends specific patterns and calls out known pitfalls.
For the complete API reference, see the Go client reference. This guide pairs with the ingestion patterns and consumption patterns guides.
The library provides two APIs:
- Native ClickHouse API — recommended for connectors. Uses the native binary protocol (port 9440 on Cloud), supports the full ClickHouse type system, and provides the most control over batch inserts and streaming.
database/sqlcompatible API — use when your code must satisfy*sql.DBor when integrating with frameworks that expect the standard Go SQL interface. It wraps the native client but trades some type fidelity for compatibility.
Installation
Connecting
Native API
Passing &tls.Config{} enables TLS with system certificate verification. For ClickHouse Cloud, always use TLS: native port 9440, HTTP port 8443. Plaintext connections are not accepted on Cloud.
ClientInfo.Products sets the User-Agent header on all queries issued by this connection, making them attributable in system.query_log.
database/sql API
Use the database/sql API when your integration framework requires a *sql.DB — for example, when using sqlx, gorm, or any library that only accepts the standard interface.
Connection options
For the native API, configure pooling and timeouts via clickhouse.Options:
Key parameters:
| Option | Recommended value | Why |
|---|---|---|
MaxOpenConns | 10–20 for BI, 2–4 for ETL | ClickHouse handles concurrency server-side; more connections rarely help |
ConnMaxLifetime | 270s | ClickHouse Cloud's keep-alive timeout is 10 minutes; 270s stays comfortably below it and avoids broken-pipe errors on idle connections |
DialTimeout | 30s | ClickHouse Cloud services on the development tier auto-pause; first connection after a pause can take several seconds |
For the database/sql API, apply equivalent settings on *sql.DB after opening:
Schema discovery
Listing columns
Query system.columns and scan into a struct. Do not use INFORMATION_SCHEMA — it does not expose is_in_sorting_key, is_in_primary_key, or the full type string with modifiers:
Always call rows.Close() (via defer) and check rows.Err() after the loop. An error mid-stream appears in rows.Err(), not in the initial conn.Query() call.
Parsing type modifiers
Strip Nullable and LowCardinality wrappers before mapping to Go types. Both can be nested in any order:
Type mapping
clickhouse-go maps ClickHouse types to Go types automatically when scanning into typed variables. Use the following as your mapping reference:
| ClickHouse type | Go type | Notes |
|---|---|---|
Int8 | int8 | |
Int16 | int16 | |
Int32 | int32 | |
Int64 | int64 | |
UInt8 | uint8 | |
UInt16 | uint16 | |
UInt32 | uint32 | |
UInt64 | uint64 | |
Float32 | float32 | |
Float64 | float64 | |
Decimal* | decimal.Decimal (shopspring) or string | No native Go decimal type; shopspring/decimal is the standard choice |
String | string | |
FixedString(N) | string | Null-padded on read — use strings.TrimRight(s, "\x00") to strip |
Date, Date32 | time.Time | Date precision only; time component is zero |
DateTime, DateTime64 | time.Time | Timezone-aware |
UUID | [16]byte or github.com/google/uuid.UUID | clickhouse-go accepts both |
IPv4, IPv6 | net.IP | |
Bool | bool | |
Array(T) | []T | Nested arrays are [][]T etc. |
Map(K, V) | map[K]V | |
Nullable(T) | *T | Scan into a pointer; nil means NULL |
For Nullable(T) columns, scan into a pointer of the appropriate type:
Querying
Streaming with rows.Next()
conn.Query() returns a streaming cursor — rows are not buffered in memory. Iterate with rows.Next():
defer rows.Close() is required — it releases the underlying connection back to the pool. If you return early (on error or when you have read enough rows), Close() must still be called.
Parameterized queries
Use positional $1, $2, ... parameters with the native API:
For named parameters, use clickhouse.Named:
Named parameters are clearer in long queries and when a value is used more than once. Parameters are passed as typed values alongside the query, not interpolated into the SQL string, so SQL injection is not possible.
Query tagging
Attach a query_id and log_comment to every query for traceability in system.query_log. Use clickhouse.Context to decorate the context:
Derive the query_id from your job and request context so it is unique and deterministic. On retry after a timeout, reuse the same query_id.
Context cancellation
Wrap long-running queries in a timeout context:
ctx.Cancel() attempts to stop the query on the client side, but does not always send a server-side KILL QUERY. Always set max_execution_time as a server-side backstop to prevent runaway queries from consuming server resources even after the client has moved on:
Inserting data
Batch insert with native API
The preferred pattern for high-throughput inserts is PrepareBatch + Append + Send:
batch.Append() serializes rows into the native binary format in memory. batch.Send() transmits the entire batch in a single network round trip. Target 10,000–100,000 rows per Send() call — smaller batches create excessive data parts and trigger Too many parts errors.
If batch.Send() fails with a network error, retry the entire batch. Use insert_deduplication_token (shown below) to make retries safe.
database/sql batch insert
When using *sql.DB, batch inserts are done by beginning a transaction, executing statements in a loop, and committing. Note: ClickHouse does not have ACID transactions — BeginTx / Commit here is a client-side batching mechanism, not a true transaction:
Rollback on error to discard the buffered batch. Do not proceed to Commit after a failed Exec — the batch state is undefined.
Async insert
For many small producers writing low-volume payloads, use conn.AsyncInsert() to let the server buffer and merge inserts before writing:
wait=true blocks until the server acknowledges the buffered data has been written to disk. With wait=false, the call returns immediately after the server receives the payload — data may not be persisted yet, and type errors silently drop the batch.
Use wait=true in all connectors that need error feedback. Reserve wait=false for fire-and-forget telemetry pipelines where you accept eventual delivery.
Idempotent inserts
Pass insert_deduplication_token as a query setting for retry-safe inserts:
Derive the token from your job and batch identifiers. On retry, send the same token — if the original insert reached the server, ClickHouse silently skips the retry.
Error handling
ClickHouse errors returned by the server are represented as *clickhouse.Exception. Type-assert to access the error code and message:
Key error codes for connector developers:
| Code | Name | Action |
|---|---|---|
| 60 | UNKNOWN_TABLE | Do not retry; surface to user |
| 81 | UNKNOWN_DATABASE | Do not retry; surface to user |
| 164 | READONLY | Do not retry; check user permissions |
| 241 | MEMORY_LIMIT_EXCEEDED | Do not retry; reduce batch size or query scope |
| 159 | TIMEOUT_EXCEEDED | May retry with a larger max_execution_time setting |
For network-level errors (where errors.As(err, &ex) returns false), retry with exponential backoff. Always reuse the same insert_deduplication_token on insert retries.
Access the full error message via ex.Message and the originating ClickHouse stack trace via ex.StackTrace when filing bug reports or surfacing details in connector logs.