Backend Development 11 min read

Idiomatic Query Building in Go: From GORM to a Custom Builder

The article explores the challenges of using GORM for layered query complexity in Go, compares it with sqlx, and proposes an idiomatic, extensible query‑builder design using functional options, complete with example implementations for SELECT, WHERE, and helper functions.

360 Tech Engineering
360 Tech Engineering
360 Tech Engineering
Idiomatic Query Building in Go: From GORM to a Custom Builder

Recently the author has been researching ways to interact with databases more easily in Go. While the sqlx package simplifies mapping rows to structs, building idiomatic conditional queries remains cumbersome, especially when using GORM’s ActiveRecord‑style API.

GORM works well for simple CRUD operations, but adding layered conditions such as a search parameter or an after date quickly leads to repetitive db.Where calls. The author demonstrates this with two GORM examples that manually check URL query parameters and modify the query.

Using sqlx directly improves the situation only slightly; the code still concatenates SQL strings and manually manages arguments, which is not scalable. The author then introduces the squirrel library as a more idiomatic way to build queries, showing how to assemble conditions with sq.Like and sq.Gt .

To achieve a truly idiomatic solution, the article proposes a custom query‑builder based on the functional‑options pattern. It defines a Query struct that tracks the statement type, table, columns, WHERE clauses, and arguments, and introduces an Option type ( func(q Query) Query ) to modify the query.

type statement uint8

type Query struct {
    stmt   statement
    table  []string
    cols   []string
    args   []interface{}
}

func Select(opts ...Option) Query {
    q := Query{stmt: select_}
    for _, opt := range opts {
        q = opt(q)
    }
    return q
}

Helper functions Columns and Table set the selected columns and target table, while WhereLike and WhereGt add LIKE and greater‑than conditions, respectively, storing placeholders and arguments.

func WhereLike(col string, val interface{}) Option {
    return func(q Query) Query {
        w := where{col: col, op: "LIKE", val: fmt.Sprintf("$%d", len(q.args)+1)}
        q.wheres = append(q.wheres, w)
        q.args = append(q.args, val)
        return q
    }
}

func WhereGt(col string, val interface{}) Option {
    return func(q Query) Query {
        w := where{col: col, op: ">", val: fmt.Sprintf("$%d", len(q.args)+1)}
        q.wheres = append(q.wheres, w)
        q.args = append(q.args, val)
        return q
    }
}

Higher‑level convenience options Search and After wrap these primitives, allowing concise construction of a query based on URL parameters.

posts := make([]Post, 0)
search := r.URL.Query().Get("search")
after := r.URL.Query().Get("after")

db := sqlx.Open("postgres", "...")

q := Select(
    Columns("*"),
    Table("posts"),
    Search("title", search),
    After(after),
)

err := db.Select(&posts, q.Build(), q.Args()...)

The author acknowledges that the builder is incomplete (e.g., Build() and Args() are not implemented) but provides enough scaffolding to illustrate an idiomatic approach to complex query construction in Go.

For the full source code, see the GitHub repository linked at the end of the article.

BackendSQLGoGormquery-buildersqlx
360 Tech Engineering
Written by

360 Tech Engineering

Official tech channel of 360, building the most professional technology aggregation platform for the brand.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.