Lightweight Go client for ClickHouse using the HTTP interface.
- HTTP interface (GET for read-only, POST for mutations)
context.Contextsupport for timeouts and cancellation- Connection pooling (reused
http.Client) - Gzip compression (request
Accept-Encoding, response decompression) - Authentication via
X-ClickHouse-User/X-ClickHouse-Keyheaders - Database selection via header and URL parameter
- Query ID and Session ID support
- Per-query ClickHouse settings
- External data for query processing
- Cluster mode with ping-based health checks and load balancing
- HTTP status code checking with structured error responses
- Type support:
int8–int64,uint8–uint64,float32,float64,string,bool,time.Time(Date and DateTime), arrays, Nullable (*string,*int64,*float64)
go get github.com/mintance/go-clickhouse
Requires Go 1.21+.
package main
import (
"context"
"fmt"
"log"
"github.com/mintance/go-clickhouse"
)
func main() {
ctx := context.Background()
conn := clickhouse.NewConn("localhost:8123", clickhouse.NewHTTPTransport())
if err := conn.Ping(ctx); err != nil {
log.Fatal(err)
}
query := clickhouse.NewQuery("SELECT number, toString(number) FROM system.numbers LIMIT 5")
iter := query.Iter(ctx, conn)
var num int
var str string
for iter.Scan(&num, &str) {
fmt.Println(num, str)
}
if err := iter.Error(); err != nil {
log.Fatal(err)
}
}ctx := context.Background()
conn := clickhouse.NewConn("localhost:8123", clickhouse.NewHTTPTransport())
query := clickhouse.NewQuery("SELECT name, date FROM clicks WHERE id = ?", 42)
iter := query.Iter(ctx, conn)
var name, date string
for iter.Scan(&name, &date) {
fmt.Println(name, date)
}
if err := iter.Error(); err != nil {
log.Fatal(err)
}ctx := context.Background()
conn := clickhouse.NewConn("localhost:8123", clickhouse.NewHTTPTransport())
query, err := clickhouse.BuildInsert("clicks",
clickhouse.Columns{"name", "date", "sourceip"},
clickhouse.Row{"Test name", "2016-01-01 21:01:01", clickhouse.Func{"IPv4StringToNum", "192.0.2.192"}},
)
if err != nil {
log.Fatal(err)
}
if err := query.Exec(ctx, conn); err != nil {
log.Fatal(err)
}ctx := context.Background()
query, err := clickhouse.BuildMultiInsert("clicks",
clickhouse.Columns{"name", "count"},
clickhouse.Rows{
clickhouse.Row{"first", 1},
clickhouse.Row{"second", 2},
},
)
if err != nil {
log.Fatal(err)
}
if err := query.Exec(ctx, conn); err != nil {
log.Fatal(err)
}transport := clickhouse.NewHTTPTransport()
conn := clickhouse.NewConnWithAuth("localhost:8123", transport, "user", "password")Or with all options:
conn := clickhouse.NewConnWithOptions(clickhouse.ConnOptions{
Host: "localhost:8123",
User: "user",
Password: "password",
Database: "mydb",
}, transport)// Track query in system.query_log
query := clickhouse.NewQuery("SELECT 1")
query.QueryID = "my-query-123"
iter := query.Iter(ctx, conn)
// Use sessions to persist SET commands across queries
set := clickhouse.NewQuery("SET max_threads = 1")
set.SessionID = "my-session"
set.Exec(ctx, conn)
get := clickhouse.NewQuery("SELECT getSetting('max_threads')")
get.SessionID = "my-session"
iter = get.Iter(ctx, conn)Pass any ClickHouse server setting as a URL parameter:
query := clickhouse.NewQuery("SELECT number FROM system.numbers LIMIT 100")
query.SetSetting("max_rows_to_read", "1000000")
query.SetSetting("max_execution_time", "60")ctx := context.Background()
query := clickhouse.NewQuery("SELECT Num, Name FROM extdata")
query.AddExternal("extdata", "Num UInt32, Name String", []byte("1\tfirst\n2\tsecond"))
iter := query.Iter(ctx, conn)
var num int
var name string
for iter.Scan(&num, &name) {
fmt.Println(num, name)
}transport := &clickhouse.HTTPTransport{
Timeout: 5 * time.Second,
Compression: true, // sends Accept-Encoding: gzip, decompresses responses
}
conn := clickhouse.NewConn("localhost:8123", transport)ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
iter := query.Iter(ctx, conn)ClickHouse Nullable columns return \N for NULL values. Use pointer types to handle them:
var name *string // Nullable(String)
var count *int64 // Nullable(Int64)
var rate *float64 // Nullable(Float64)
iter.Scan(&name, &count, &rate)
// name == nil means NULLerr := query.Exec(ctx, conn)
if err != nil {
var dbErr *clickhouse.DBError
if errors.As(err, &dbErr) {
fmt.Printf("ClickHouse error %d: %s\n", dbErr.Code(), dbErr.Message())
fmt.Println("Full response:", dbErr.Response())
} else {
// Transport error, HTTP error, etc.
fmt.Println("Error:", err)
}
}Non-200 HTTP responses are also returned as errors. If the response body contains a ClickHouse error code, it is parsed into a *DBError. Otherwise, you get a generic error like "clickhouse: HTTP 403: Forbidden".
Cluster manages multiple connections for load balancing across servers with the same Distributed table.
cluster.Check()— pings all connections and updates the active setcluster.CheckCtx(ctx)— same with context supportcluster.ActiveConn()— returns a random active connectioncluster.IsDown()— returns true if no connections are activecluster.OnCheckError(func)— callback when a connection fails its health check
Important: Call Check() at least once after initialization. We recommend calling it continuously so ActiveConn() always returns a healthy connection.
transport := clickhouse.NewHTTPTransport()
conn1 := clickhouse.NewConn("host1", transport)
conn2 := clickhouse.NewConn("host2", transport)
cluster := clickhouse.NewCluster(conn1, conn2)
cluster.OnCheckError(func(c *clickhouse.Conn) {
log.Printf("ClickHouse connection failed %s", c.Host)
})
// Ping connections every second
go func() {
for {
cluster.Check()
time.Sleep(time.Second)
}
}()
// Use in requests
conn := cluster.ActiveConn()
if conn != nil {
query.Exec(ctx, conn)
}transport := clickhouse.NewHTTPTransport()
transport.Timeout = 5 * time.Second
conn := clickhouse.NewConn("localhost:8123", transport)HTTPTransport reuses a single http.Client with connection pooling (100 max idle connections, 10 per host, 90s idle timeout). No configuration needed — just reuse the same transport instance.
| ClickHouse Type | Go Type | Marshal | Unmarshal |
|---|---|---|---|
| Int8–Int64 | int8–int64 |
Yes | Yes |
| UInt8–UInt64 | uint8–uint64 |
Yes | Yes |
| Float32, Float64 | float32, float64 |
Yes | Yes |
| String | string |
Yes | Yes |
| Bool (UInt8) | bool |
Yes | Yes |
| Date | time.Time |
Yes | Yes |
| DateTime | time.Time |
Yes | Yes |
| Array(T) | []int, []string, Array |
Yes | Yes |
| Nullable(String) | *string |
— | Yes |
| Nullable(Int64) | *int64 |
— | Yes |
| Nullable(Float64) | *float64 |
— | Yes |
See CONTRIBUTING.md for development setup, how to run tests, and PR guidelines.