chconn - ClickHouse Native Protocol Driver for Go

July 2, 2026 · View on GitHub

Go Reference codecov Go Report Card Actions Status FOSSA Status

chconn - ClickHouse Native Protocol Driver for Go

chconn is a pure Go driver for ClickHouse using the native TCP protocol. It is designed for high-performance, column-oriented data access with full generics support.

If you have any suggestion or comment, please feel free to open an issue.

Installation

go get github.com/vahid-sohrabloo/chconn/v3

Requires the latest two stable Go releases.

Quick Start

package main

import (
	"context"
	"fmt"

	"github.com/vahid-sohrabloo/chconn/v3/chpool"
	"github.com/vahid-sohrabloo/chconn/v3/column"
)

func main() {
	conn, err := chpool.New("clickhouse://default:@localhost:9000/default")
	if err != nil {
		panic(err)
	}
	defer conn.Close()

	// Create table
	err = conn.Exec(context.Background(), `CREATE TABLE IF NOT EXISTS example (
		id    UInt64,
		name  String
	) Engine=Memory`)
	if err != nil {
		panic(err)
	}

	// Insert
	colID := column.New[uint64]()
	colName := column.NewString()
	colID.Append(1)
	colName.Append("Alice")
	colID.Append(2)
	colName.Append("Bob")

	err = conn.Insert(context.Background(), "INSERT INTO example (id, name) VALUES", colID, colName)
	if err != nil {
		panic(err)
	}

	// Select
	colIDRead := column.New[uint64]()
	colNameRead := column.NewString()
	selectStmt, err := conn.Select(context.Background(), "SELECT id, name FROM example", colIDRead, colNameRead)
	if err != nil {
		panic(err)
	}
	defer selectStmt.Close()

	for selectStmt.Next() {
		for i := 0; i < selectStmt.RowsInBlock(); i++ {
			fmt.Printf("id=%d name=%s\n", colIDRead.Row(i), colNameRead.Row(i))
		}
	}
	if err := selectStmt.Err(); err != nil {
		panic(err)
	}
}

Core Patterns

Column-Oriented Insert

The fastest way to insert data. Append values to typed columns and insert in bulk:

col1 := column.New[uint64]()
col2 := column.NewString()
col3 := column.New[uint64]().Nullable()

// Pre-allocate write buffers for large inserts
col1.SetWriteBufferSize(1_000_000)
col2.SetWriteBufferSize(1_000_000)
col3.SetWriteBufferSize(1_000_000)

for i := range 1_000_000 {
    col1.Append(uint64(i))
    col2.Append(fmt.Sprintf("row_%d", i))
    if i%2 == 0 {
        col3.Append(uint64(i))
    } else {
        col3.AppendNil()
    }
}

err := conn.Insert(ctx, "INSERT INTO table (id, name, value) VALUES", col1, col2, col3)

Column-Oriented Select

Read data block by block for maximum throughput:

colID := column.New[uint64]()
colName := column.NewString()

selectStmt, err := conn.Select(ctx, "SELECT id, name FROM table", colID, colName)
if err != nil {
    return err
}
defer selectStmt.Close()

var ids []uint64
var names []string
for selectStmt.Next() {
    ids = colID.Read(ids)
    names = colName.Read(names)
}
if err := selectStmt.Err(); err != nil {
    return err
}

Row-Oriented Query

Familiar SQL-style row iteration with Query and Scan:

rows, err := conn.Query(ctx, "SELECT id, name FROM users WHERE age > {age: UInt32}",
    chconn.IntParameter("age", 18),
)
if err != nil {
    return err
}
defer rows.Close()

for rows.Next() {
    var id uint64
    var name string
    if err := rows.Scan(&id, &name); err != nil {
        return err
    }
    fmt.Printf("%d: %s\n", id, name)
}
return rows.Err()

Struct Scanning

Map results directly to Go structs by field name:

type User struct {
    ID    uint64
    Name  string
    Email string
}

// Collect all rows into a slice
users, err := chconn.QueryAll[User](ctx, conn, "SELECT id, name, email FROM users")

// Get exactly one row
user, err := chconn.QueryExactlyOne[User](ctx, conn,
    "SELECT id, name, email FROM users WHERE id = {id: UInt64}",
    chconn.UintParameter("id", 42),
)

// Iterator pattern
for user, err := range chconn.QueryIter[User](ctx, conn, "SELECT id, name, email FROM users") {
    if err != nil {
        return err
    }
    fmt.Println(user.Name)
}

Scanning options:

  • RowToStructByName[T] — match columns to struct fields by name (case-insensitive)
  • RowToStructByNameLax[T] — same, but tolerates extra/missing fields
  • RowToStructByPos[T] — match by column position
  • RowTo[T] — auto-detect: struct by name, map[string]any, or scalar

Streaming Insert

For multi-batch inserts or row-by-row appending:

stmt, err := conn.InsertStream(ctx, "INSERT INTO table (id, name) VALUES")
if err != nil {
    return err
}

// Row-by-row append
for i := range 1000 {
    if err := stmt.Append(uint64(i), fmt.Sprintf("name_%d", i)); err != nil {
        return err
    }
}

// Or write column batches
col1 := column.New[uint64]()
col2 := column.NewString()
col1.Append(1001)
col2.Append("batch_row")
if err := stmt.Write(ctx, col1, col2); err != nil {
    return err
}

return stmt.Flush(ctx)

Parameterized Queries

Type-safe query parameters using ClickHouse native parameter syntax:

rows, err := conn.Query(ctx,
    "SELECT * FROM events WHERE ts > {start: DateTime} AND level = {level: String}",
    chconn.StringParameter("start", "2024-01-01 00:00:00"),
    chconn.StringParameter("level", "error"),
)

Available parameter functions: IntParameter, UintParameter, Float32Parameter, Float64Parameter, StringParameter, and their slice variants (IntSliceParameter, etc.).

Features

Connection Pool

pool, err := chpool.New("clickhouse://user:password@localhost:9000/mydb")
// or with full config:
config, _ := chpool.ParseConfig("clickhouse://user:password@localhost:9000/mydb")
config.MaxConns = 10
config.MinConns = 2
config.MaxConnLifetime = time.Hour
config.MaxConnIdleTime = 30 * time.Minute
config.HealthCheckPeriod = time.Minute

pool, err := chpool.NewWithConfig(ctx, config)

The pool implements the same Select, Insert, Query, Exec methods as a single connection.

Supported Types

ClickHouse TypeGo Column
Boolcolumn.New[bool]()
UInt8/16/32/64/128/256column.New[uint8](), column.New[types.Uint128](), etc.
Int8/16/32/64/128/256column.New[int8](), column.New[types.Int128](), etc.
Float32/64column.New[float32](), column.New[float64]()
BFloat16column.New[types.BFloat16]()
Decimal32/64/128/256column.New[types.Decimal32](), etc.
Stringcolumn.NewString()
FixedString(N)column.New[[N]byte]()
Date, Date32column.NewDate[types.Date](), column.NewDate[types.Date32]()
DateTime, DateTime64column.NewDate[types.DateTime](), column.NewDate[types.DateTime64]()
Time, Time64column.New[types.ChTime](), column.New[types.ChTime64]()
UUIDcolumn.New[types.UUID]()
IPv4, IPv6column.New[types.IPv4](), column.New[types.IPv6]()
Enum8, Enum16column.New[int8](), column.New[int16]()
Array(T)col.Array()
Nullable(T)col.Nullable()
LowCardinality(T)col.LowCardinality()
Map(K, V)column.NewMap[K, V](keyCol, valCol)
Tuple(T1, ..., Tn)column.NewTuple(cols...) or typed column.NewTuple2[T](col1, col2)
Nestedcolumn.NewNested(cols...)
JSONcolumn.NewJSON()
Variant(T1, ..., Tn)column.NewVariant(cols...)
Dynamiccolumn.NewDynamic(cols...)
Point, Ring, Polygon, MultiPolygoncolumn.NewPoint()
Nothingcolumn.NewNothing()

Compression

// LZ4 (recommended)
config.Compress = chconn.CompressLZ4

// ZSTD
config.Compress = chconn.CompressZSTD

Or via connection string: clickhouse://localhost:9000/mydb?compress=lz4

TLS/SSL

clickhouse://user:pass@host:9440/db?sslmode=verify-full&sslrootcert=/path/to/ca.pem

SSL modes: disable, allow, prefer (default), require, verify-ca, verify-full

Code Generator (chgen)

Generate Go model structs and column boilerplate from ClickHouse tables:

# Install
go install github.com/vahid-sohrabloo/chconn/v3/cmd/chgen@latest

# Generate model from live table
chgen model -dsn "clickhouse://localhost:9000" -table users -out models/user.go

# Generate model from SQL file
chgen model -sql create_users.sql -out models/user.go

# Generate column declarations for insert/select
chgen columns -dsn "clickhouse://localhost:9000" -table users -out columns/user_columns.go

SQL Builder

sb := sqlbuilder.NewSelectBuilder()
query, _ := sb.Select("id", "name", "email").
    From("users").
    Where("age > 18").
    PreWhere("active = 1").
    OrderBy("name").
    Limit(100).
    Build()

Supports: Select, From, Where, PreWhere, Having, GroupBy, OrderBy, Limit, Offset, Distinct, Final, Join (all types), ArrayJoin.

JSON Export

Export query results as JSON:

exporter := format.NewJSON(1000, func(data []byte, cols []column.ColumnCore) {
    os.Stdout.Write(data)
})
selectStmt, _ := conn.Select(ctx, "SELECT * FROM table", cols...)
exporter.Read(selectStmt)        // JSON objects
exporter.ReadCompact(selectStmt) // JSON arrays (compact)
exporter.ReadEachRow(selectStmt) // One JSON object per row

Progress and Profile Callbacks

Monitor query execution in real time:

opts := &chconn.QueryOptions{
    QueryID: "my-query-123",
    OnProgress: func(p *chconn.Progress) {
        fmt.Printf("Read %d rows, %d bytes\n", p.ReadRows, p.ReadBytes)
    },
    OnProfile: func(p *chconn.Profile) {
        fmt.Printf("Result: %d rows in %d blocks\n", p.Rows, p.Blocks)
    },
}
selectStmt, err := conn.SelectWithOption(ctx, "SELECT ...", opts, cols...)

Error Handling

var chErr *chconn.ChError
if errors.As(err, &chErr) {
    fmt.Printf("ClickHouse error %d: %s\n", chErr.Code, chErr.Message)
    fmt.Println(chErr.StackTrace)
}

// Sentinel errors
if errors.Is(err, chconn.ErrNoRows) { ... }
if errors.Is(err, chconn.ErrTooManyRows) { ... }

Context Support

All operations accept context.Context for timeouts and cancellation:

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

err := conn.Insert(ctx, "INSERT INTO ...", cols...)

Supported Versions

  • Go: latest two stable releases
  • ClickHouse: 24.3+

Tested in CI against ClickHouse 24.3, 24.8, 25.3, 25.8, and 26.2.

Documentation

For more information, please see the documentation and Go package reference.

Benchmarks

For comparison with other Go ClickHouse drivers, see the benchmark repository and ch-bench results.

name \ time/op            chconn       chgo       go-clickhouse     uptrace
TestSelect100MUint64-16    150ms      154ms           8019ms         3045ms
TestSelect10MString-16     271ms      447ms            969ms          822ms
TestInsert10M-16           198ms      514ms            561ms          304ms

name \ alloc/op           chconn       chgo       go-clickhouse     uptrace
TestSelect100MUint64-16    111kB      262kB       3202443kB        800941kB
TestSelect10MString-16    1.63MB     1.79MB        1626.51MB       241.03MB
TestInsert10M-16          26.0MB    283.7MB         1680.4MB        240.2MB

name \ allocs/op          chconn       chgo       go-clickhouse     uptrace
TestSelect100MUint64-16      35       6683       200030937       100006069
TestSelect10MString-16       49       1748        30011991        20001120
TestInsert10M-16             26         80             224              50

License

FOSSA Status