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
Dialectorimplementation 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
Initializemethod receiving a*DBgives 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.DBthat 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),ConnPoolin dialectors for custom pools. - Design quality: Excellent ISP example — 4 methods, each maps to a single
database/sqloperation. 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(theCommonMigratorstruct) 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 theStatement.Clausesmap (e.g.,"WHERE","ORDER BY").Build(Builder)renders the clause to SQL via the dialect-awareBuilder.MergeClausehandles deduplication when the same clause is added multiple times (e.g., multipleWherecalls merge into oneAND-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
MergeClausemethod is the clever part — it means you can addWhereten 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 theDialector.DefaultValueOfreturn type, and inclause.Expr(raw SQL with interpolation). Theclause.Builderinterface itself generatesExpression.Buildcalls 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
Builderparameter 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.Buildreceives. Provides the dialect-aware output stream:WriteQuotedquotes identifiers usingDialector.QuoteTo,AddVarbinds parameters usingDialector.BindVarToand appends them toStatement.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:
*Statementis the canonical implementation (carries theDialectorand collects SQL + vars). - Design quality: Small, precise. Embeds
Writerfor 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
LogModemethod returningInterfaceallows chaining (e.g.,logger.Default.LogMode(logger.Info)) without type assertions.Traceis the SQL-specific log method — the lazyfc 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), andlogger.Discard(no-op). Users can implement their own. - Design quality: Well-designed.
Tracewith a function argument is idiomatic for expensive operations that shouldn’t run at silent log levels.LogModereturningInterfaceis 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
NamerviaConfig.NamingStrategyto control the entire naming policy. The default implementation (NamingStrategystruct) uses plural table names, snake_case columns, and optional prefix/replacement rules. - Implementations:
schema.NamingStrategy(default, verified withvar _ 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>.Scandeserializes a database value into a Go struct field;Valueserializes a Go value for storage. Registered in a globalsync.Mapby name. Default serializers:json,gob,unixtime. - Implementations:
schema.JSONSerializer,schema.GobSerializer,schema.UnixSecondSerializer. Users register custom serializers viaschema.RegisterSerializer. - Design quality: Good composition —
SerializerValuerInterface(write path) is broken out separately so types that only need to customize thedriver.Valuerpath can implement just that. Thesync.Mapregistry 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) errormethod, 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.Pluginsmap (keyed byName()) and calldb.Callback()methods duringInitializeto 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
Initializemethod receiving a live*DBgives 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 typedInterface[T]whereTis the model struct. The interface hierarchy enforces valid method sequences:CreateInterface[T]is returned afterWhere, restricting which operations are available depending on context.ExecInterface[T]is the base (read operations), composed intoChainInterface(filtering + exec) andCreateInterface(filtering + exec + create). - Implementations:
g[T]/chainG[T]/createG[T]/execG[T](unexported, returned only viaG[T](db)). - Design quality: Ambitious. The state machine via interface types (you get
SetUpdateOnlyInterfaceafter callingSet, limiting you to justUpdate) is clever type-system enforcement of valid API sequences. The composition via embedding (CreateInterfaceembedsExecInterface) is well-structured. However, the interface surface area is large and somewhat duplicates the non-generic*DBAPI — 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 genericChainInterface[T](~20 methods, full query DSL typed). The callback hook interfaces are all exactly 1 method. - Embedding: Used deliberately in several places:
clause.Builderembedsclause.Writer(adds dialect ops to basic I/O)schema.SerializerInterfaceembedsSerializerValuerInterface(separates write from read path)gorm.TxembedsConnPool+TxCommitter(composes a transaction from its parts)generics.CreateInterface[T]embedsExecInterface[T](layered capability)
- Implicit satisfaction: All interfaces are satisfied implicitly (no
Registercall, noimplementsannotation). Go’s structural typing is used throughout. The only explicit check isvar _ Namer = (*NamingStrategy)(nil)inschema/naming.go— a single compile-time assertion. - Stdlib interfaces used:
driver.Valuer—serializer.Value()for writing to DBsql.Scanner—serializer.Scan()for reading from DBio.Writer(viaclause.Writer) — SQL output streamfmt.Stringer— not used explicitly butclause.Expr.SQLis a stringcontext.Context— pervasive, passed through all significant interfaces
Key abstractions#
Dialector— The primary extensibility seam. Everything database-specific lives behind this interface. BecauseDialector.Initializeis 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.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.clause.Expression/clause.Interface— The typed SQL AST. The entire SQL construction system flows through these two interfaces.clause.Interfaceis the top-level clause (WHERE, SELECT),clause.Expressionis any SQL fragment. Together they make queries composable, dialect-safe, and introspectable — which also enables theDryRunmode to capture the SQL without executing it.Migrator— The schema management contract. By defining migration as a large interface backed bymigrator.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’sALTER TABLEvs PostgreSQL’sALTER COLUMN TYPE USING).Callback hook interfaces (
BeforeCreate,AfterCreate, etc.) — The behavior injection points for model structs. Nine single-method interfaces incallbacks/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 aclause.Expron 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.