GORM — Interfaces#

Interface catalog#

Dialector#

  • Package: gorm.io/gorm
  • File: interfaces.go:12
  • Methods:
    Name() string
    Initialize(*DB) error
    Migrator(db *DB) Migrator
    DataTypeOf(*schema.Field) string
    DefaultValueOf(*schema.Field) clause.Expression
    BindVarTo(writer clause.Writer, stmt *Statement, v interface{})
    QuoteTo(clause.Writer, string)
    Explain(sql string, vars ...interface{}) string
  • Purpose: The central extension point — encapsulates everything that varies between databases. A Dialector implementation is the entire “database driver” from GORM’s perspective: it opens the connection, registers CRUD callbacks, provides the migrator, maps Go types to DB types, and handles dialect-specific SQL rendering (bind variables, identifier quoting).
  • Implementations: All live in separate modules: gorm.io/driver/postgres, gorm.io/driver/mysql, gorm.io/driver/sqlite, gorm.io/driver/sqlserver. gorm.io/driver/clickhouse, etc.
  • Design quality: Well-scoped at 8 methods. Each method is independently useful and covers a distinct dialect concern. The Initialize method receiving a *DB gives dialects full access to register callbacks and set the connection pool — a deliberate inversion. Slightly verbose but not bloated.

ConnPool#

  • Package: gorm.io/gorm
  • File: interfaces.go:34
  • Methods:
    PrepareContext(ctx context.Context, query string) (*sql.Stmt, error)
    ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
    QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
    QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
  • Purpose: Abstracts the raw database connection pool. Mirrors the surface of *sql.DB that GORM actually uses, allowing *sql.DB, *sql.Tx, PreparedStmtDB (LRU-cache layer), and mock pools to be used interchangeably.
  • Implementations: *sql.DB (standard), *sql.Tx (transactions), PreparedStmtDB (prepared statement cache, internal), ConnPool in dialectors for custom pools.
  • Design quality: Excellent ISP example — 4 methods, each maps to a single database/sql operation. Adding the prepared-statement cache by wrapping rather than embedding is a direct consequence of this narrow interface.

Migrator#

  • Package: gorm.io/gorm
  • File: migrator.go:68
  • Methods (25+):
    AutoMigrate(dst ...interface{}) error
    CurrentDatabase() string
    FullDataTypeOf(*schema.Field) clause.Expr
    GetTypeAliases(databaseTypeName string) []string
    CreateTable/DropTable/HasTable/RenameTable/GetTables/TableType(...)
    AddColumn/DropColumn/AlterColumn/MigrateColumn/MigrateColumnUnique/HasColumn/RenameColumn/ColumnTypes(...)
    CreateView/DropView(...)
    CreateConstraint/DropConstraint/HasConstraint(...)
    CreateIndex/DropIndex/HasIndex/RenameIndex/GetIndexes(...)
  • Purpose: Defines the complete schema migration contract. Each dialect’s migrator implements this interface, with migrator.Migrator (the CommonMigrator struct) providing portable fallback implementations that dialects embed and selectively override.
  • Implementations: migrator.Migrator (base/common), then overridden per dialect. The common migrator handles portable SQL; dialects override database-specific DDL.
  • Design quality: Intentionally broad — this is the migration DSL. The breadth is justified (full schema lifecycle: create, alter, drop, introspect across tables/columns/views/constraints/indexes). Not a violation of ISP because consumers always need the full migration surface. Could theoretically be split into sub-interfaces (TableMigrator, ColumnMigrator, etc.) but GORM doesn’t do this — dialects get one interface to implement.

clause.Interface#

  • Package: gorm.io/gorm/clause
  • File: clause/clause.go:5
  • Methods:
    Name() string
    Build(Builder)
    MergeClause(*Clause)
  • Purpose: Contract that every SQL clause type must satisfy. Name() is the clause key in the Statement.Clauses map (e.g., "WHERE", "ORDER BY"). Build(Builder) renders the clause to SQL via the dialect-aware Builder. MergeClause handles deduplication when the same clause is added multiple times (e.g., multiple Where calls merge into one AND-joined expression).
  • Implementations: clause.Select, clause.Where, clause.OrderBy, clause.Limit, clause.GroupBy, clause.Join, clause.OnConflict, clause.Returning, clause.With, clause.For, and more — every SQL clause has a concrete type.
  • Design quality: Perfect 3-method interface. The MergeClause method is the clever part — it means you can add Where ten times and the clause itself knows how to coalesce, rather than the engine having to handle deduplication logic.

clause.Expression#

  • Package: gorm.io/gorm/clause
  • File: clause/expression.go:11
  • Methods:
    Build(Builder)
  • Purpose: The atomic SQL fragment contract. Anything that can render SQL is an Expression. Used everywhere — in clause bodies (WHERE expr, ORDER BY expr), as values in parameterized queries, as the Dialector.DefaultValueOf return type, and in clause.Expr (raw SQL with interpolation). The clause.Builder interface itself generates Expression.Build calls recursively.
  • Implementations: clause.Expr (raw SQL), clause.NamedExpr, clause.Eq/clause.Neq/clause.Lt/etc. (comparison operators), clause.IN, clause.Like, clause.AndConditions, clause.OrConditions, clause.Not, clause.Column, clause.Table, and many more.
  • Design quality: As minimal as possible (1 method). The Builder parameter provides quoting and binding — the expression just calls into it. This is the foundation of the typed SQL AST.

clause.Builder#

  • Package: gorm.io/gorm/clause
  • File: clause/clause.go:20
  • Methods:
    // embedded: Writer
    WriteByte(byte) error
    WriteString(string) (int, error)
    // own
    WriteQuoted(field interface{})
    AddVar(Writer, ...interface{})
    AddError(error) error
  • Purpose: The rendering context that Expression.Build receives. Provides the dialect-aware output stream: WriteQuoted quotes identifiers using Dialector.QuoteTo, AddVar binds parameters using Dialector.BindVarTo and appends them to Statement.Vars. Expressions never call the Dialector directly — they go through Builder. This is how dialect-specific rendering is injected without coupling clause types to any dialect.
  • Implementations: *Statement is the canonical implementation (carries the Dialector and collects SQL + vars).
  • Design quality: Small, precise. Embeds Writer for byte/string output and adds the two dialect-sensitive operations. Clean separation between raw bytes and structured SQL tokens.

logger.Interface#

  • Package: gorm.io/gorm/logger
  • File: logger/logger.go:64
  • Methods:
    LogMode(LogLevel) Interface
    Info(context.Context, string, ...interface{})
    Warn(context.Context, string, ...interface{})
    Error(context.Context, string, ...interface{})
    Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error)
  • Purpose: Logging abstraction used throughout GORM. The LogMode method returning Interface allows chaining (e.g., logger.Default.LogMode(logger.Info)) without type assertions. Trace is the SQL-specific log method — the lazy fc func() argument defers SQL string building to avoid cost if log level is suppressed.
  • Implementations: logger.Logger (default colorized stdout), logger.SlogLogger (Go 1.21 slog adapter), and logger.Discard (no-op). Users can implement their own.
  • Design quality: Well-designed. Trace with a function argument is idiomatic for expensive operations that shouldn’t run at silent log levels. LogMode returning Interface is a minor ergonomic decision that works well.

schema.Namer#

  • Package: gorm.io/gorm/schema
  • File: schema/naming.go:16
  • Methods:
    TableName(table string) string
    SchemaName(table string) string
    ColumnName(table, column string) string
    JoinTableName(joinTable string) string
    RelationshipFKName(Relationship) string
    CheckerName(table, column string) string
    IndexName(table, column string) string
    UniqueName(table, column string) string
  • Purpose: Naming convention strategy for converting Go struct/field names to database identifiers. Users can supply a custom Namer via Config.NamingStrategy to control the entire naming policy. The default implementation (NamingStrategy struct) uses plural table names, snake_case columns, and optional prefix/replacement rules.
  • Implementations: schema.NamingStrategy (default, verified with var _ Namer = (*NamingStrategy)(nil) compile-time check).
  • Design quality: Covers all naming scenarios a relational schema needs. 8 methods, each independently overridable. The explicit compile-time interface check is a good practice signal.

schema.SerializerInterface#

  • Package: gorm.io/gorm/schema
  • File: schema/serializer.go:63
  • Methods:
    // embedded: SerializerValuerInterface
    Value(ctx context.Context, field *Field, dst reflect.Value, fieldValue interface{}) (interface{}, error)
    // own
    Scan(ctx context.Context, field *Field, dst reflect.Value, dbValue interface{}) error
  • Purpose: Pluggable serialization/deserialization for model fields tagged with serializer:<name>. Scan deserializes a database value into a Go struct field; Value serializes a Go value for storage. Registered in a global sync.Map by name. Default serializers: json, gob, unixtime.
  • Implementations: schema.JSONSerializer, schema.GobSerializer, schema.UnixSecondSerializer. Users register custom serializers via schema.RegisterSerializer.
  • Design quality: Good composition — SerializerValuerInterface (write path) is broken out separately so types that only need to customize the driver.Valuer path can implement just that. The sync.Map registry pattern makes serializers globally reusable by tag name.

Callback hook interfaces (callbacks package)#

  • Package: gorm.io/gorm/callbacks
  • File: callbacks/interfaces.go
  • Interfaces: BeforeCreateInterface, AfterCreateInterface, BeforeUpdateInterface, AfterUpdateInterface, BeforeSaveInterface, AfterSaveInterface, BeforeDeleteInterface, AfterDeleteInterface, AfterFindInterface
  • Methods (each has 1):
    BeforeCreate(*gorm.DB) error   // AfterCreate, BeforeUpdate, etc. follow same pattern
    AfterFind(*gorm.DB) error
  • Purpose: Model lifecycle hooks. If a model struct implements any of these interfaces, GORM’s callback pipeline automatically calls the method at the appropriate stage. This is how user models add behavior without subclassing or registration calls.
  • Implementations: Any user model struct that defines a BeforeCreate(*gorm.DB) error method, etc.
  • Design quality: Perfect ISP. 9 single-method interfaces — a model implements only the hooks it needs. No cost for hooks not implemented. The type assertion is done once during callback registration, not on every query.

Plugin#

  • Package: gorm.io/gorm
  • File: interfaces.go:24
  • Methods:
    Name() string
    Initialize(*DB) error
  • Purpose: Extension point for third-party GORM plugins (e.g., soft-delete, optimistic locking, tracing, caching). Plugins register themselves into Config.Plugins map (keyed by Name()) and call db.Callback() methods during Initialize to register their hooks.
  • Implementations: gorm.io/plugin/dbresolver, gorm.io/plugin/optimisticlock, gorm.io/plugin/prometheus, and many community plugins.
  • Design quality: Minimal and effective (2 methods). The Initialize method receiving a live *DB gives plugins full access to the callback registration API.

generics.Interface[T] and friends#

  • Package: gorm.io/gorm
  • File: generics.go:38
  • Interfaces: Interface[T], CreateInterface[T], ChainInterface[T], ExecInterface[T], SetUpdateOnlyInterface[T], SetCreateOrUpdateInterface[T], JoinBuilder, PreloadBuilder
  • Methods: Large — ChainInterface[T] has ~15 chainable methods + 4 terminal operations. ExecInterface[T] has 7 query-execution methods.
  • Purpose: Type-safe generics API introduced to eliminate the need to pass interface{} model pointers. G[T](db) returns a typed Interface[T] where T is the model struct. The interface hierarchy enforces valid method sequences: CreateInterface[T] is returned after Where, restricting which operations are available depending on context. ExecInterface[T] is the base (read operations), composed into ChainInterface (filtering + exec) and CreateInterface (filtering + exec + create).
  • Implementations: g[T] / chainG[T] / createG[T] / execG[T] (unexported, returned only via G[T](db)).
  • Design quality: Ambitious. The state machine via interface types (you get SetUpdateOnlyInterface after calling Set, limiting you to just Update) is clever type-system enforcement of valid API sequences. The composition via embedding (CreateInterface embeds ExecInterface) is well-structured. However, the interface surface area is large and somewhat duplicates the non-generic *DB API — this is a usability/type-safety tradeoff.

Interface patterns#

  • Size distribution: Very small on average. Most interfaces have 1-4 methods. The exceptions are Migrator (~25 methods, full migration DSL) and the generic ChainInterface[T] (~20 methods, full query DSL typed). The callback hook interfaces are all exactly 1 method.
  • Embedding: Used deliberately in several places:
    • clause.Builder embeds clause.Writer (adds dialect ops to basic I/O)
    • schema.SerializerInterface embeds SerializerValuerInterface (separates write from read path)
    • gorm.Tx embeds ConnPool + TxCommitter (composes a transaction from its parts)
    • generics.CreateInterface[T] embeds ExecInterface[T] (layered capability)
  • Implicit satisfaction: All interfaces are satisfied implicitly (no Register call, no implements annotation). Go’s structural typing is used throughout. The only explicit check is var _ Namer = (*NamingStrategy)(nil) in schema/naming.go — a single compile-time assertion.
  • Stdlib interfaces used:
    • driver.Valuerserializer.Value() for writing to DB
    • sql.Scannerserializer.Scan() for reading from DB
    • io.Writer (via clause.Writer) — SQL output stream
    • fmt.Stringer — not used explicitly but clause.Expr.SQL is a string
    • context.Context — pervasive, passed through all significant interfaces

Key abstractions#

  1. Dialector — The primary extensibility seam. Everything database-specific lives behind this interface. Because Dialector.Initialize is where CRUD callbacks are registered, dialects are not passive adapters but active configurers of the pipeline. This is GORM’s inversion of control: the core doesn’t know which database is used; the dialect wires itself in.

  2. ConnPool — The database/sql abstraction layer. Mirrors *sql.DB’s used surface, enabling the prepared-statement cache (PreparedStmtDB) to be injected transparently and enabling test doubles without a real database. Four methods — no more, no less.

  3. clause.Expression / clause.Interface — The typed SQL AST. The entire SQL construction system flows through these two interfaces. clause.Interface is the top-level clause (WHERE, SELECT), clause.Expression is any SQL fragment. Together they make queries composable, dialect-safe, and introspectable — which also enables the DryRun mode to capture the SQL without executing it.

  4. Migrator — The schema management contract. By defining migration as a large interface backed by migrator.CommonMigrator (a struct dialects embed), GORM achieves maximum code reuse for portable DDL while allowing dialect-specific overrides for any individual operation (e.g., MySQL’s ALTER TABLE vs PostgreSQL’s ALTER COLUMN TYPE USING).

  5. Callback hook interfaces (BeforeCreate, AfterCreate, etc.) — The behavior injection points for model structs. Nine single-method interfaces in callbacks/ allow zero-cost, opt-in lifecycle hooks. This is the primary way user code extends GORM behavior at the model level — no decorators, no annotations, just interface implementation.

Interface-driven extensibility#

GORM uses interfaces at every layer of its extensibility model:

  • Dialect plugins implement Dialector (full database support).
  • GORM plugins implement Plugin (hook-based extensions like caching, tracing, sharding).
  • Model hooks implement the 9 callback interfaces in callbacks/ (per-operation lifecycle).
  • Custom naming implements schema.Namer (naming conventions).
  • Custom serialization implements schema.SerializerInterface (field-level serialization).
  • Custom data types implement schema.GormDataTypeInterface, schema.CreateClausesInterface, schema.QueryClausesInterface, schema.UpdateClausesInterface, schema.DeleteClausesInterface — allowing custom Go types to contribute SQL clauses for each operation type.
  • Custom SQL values implement gorm.Valuer (a type that generates a clause.Expr on write).

This layered interface system means nearly every behavior in GORM can be extended or replaced without forking the core. The Dialector is the broadest contract (replaces an entire database backend); the single-method callback interfaces are the narrowest (add one hook to one operation).

The generics API (G[T](db)) adds a second, type-safe surface on top of the existing interface system, using interface hierarchy to encode valid API state transitions at compile time — a notable evolution beyond the reflection-based original API.