GORM — Structure#
Layout pattern#
Flat Library (root-package-heavy)
GORM uses no cmd/ directory and produces no binary — it is a pure Go library. The root package (gorm) hosts all primary user-facing types and the API surface (DB, Config, Statement, callbacks, interfaces). Specialized domains (SQL clause building, schema reflection, logging, migration) are extracted into sub-packages. There is no pkg/ layer; public API lives directly in the root.
Directory map#
gorm/ # Root package — public API, core types
├── gorm.go # DB struct, Open(), Session(), Config, Option interface
├── interfaces.go # Core interfaces: Dialector, ConnPool, Plugin, Tx, Valuer, etc.
├── callbacks.go # Callback manager and processor registration
├── chainable_api.go # Chainable query builder methods (Where, Limit, Order, …)
├── finisher_api.go # Terminal query methods (Find, First, Create, Save, Delete, …)
├── association.go # Association (has many/belongs to) operations
├── statement.go # Statement struct — SQL builder state per operation
├── scan.go # Result scanning logic (rows → structs)
├── prepare_stmt.go # Prepared statement cache (PreparedStmtDB)
├── migrator.go # AutoMigrate entry point, Migrator interface
├── model.go # gorm.Model (ID, CreatedAt, UpdatedAt, DeletedAt)
├── soft_delete.go # DeletedAt type, soft-delete hook logic
├── generics.go # Generic wrappers: Find[T], First[T], etc. (Go 1.18)
├── errors.go # Sentinel error values (ErrRecordNotFound, ErrInvalidDB, …)
│
├── callbacks/ # CRUD operation implementations (called via processor pipeline)
│ ├── callbacks.go # RegisterDefaultCallbacks — wires all CRUD callbacks
│ ├── create.go # Create callbacks (BeforeSave, Insert, AfterSave)
│ ├── query.go # Query callbacks (BeforeFind, Scan, AfterFind)
│ ├── update.go # Update callbacks (BeforeSave, Updates, AfterSave)
│ ├── delete.go # Delete callbacks (BeforeDelete, Delete, AfterDelete)
│ ├── row.go # Raw row callbacks
│ ├── raw.go # Raw SQL callbacks
│ ├── preload.go # Association preloading (Preload)
│ ├── associations.go # Association save/delete callbacks
│ ├── transaction.go # Transaction wrapping callbacks
│ ├── helper.go # Shared callback helpers
│ ├── callmethod.go # User-defined hook invocation (BeforeCreate, etc.)
│ └── interfaces.go # Callback-local interface definitions
│
├── clause/ # SQL clause AST — composable typed SQL building
│ ├── clause.go # Clause, ClauseBuilder, Expression, Writer interfaces
│ ├── expression.go # Expr, NamedExpr, Column, Table, IN, EQ, AND, OR, …
│ ├── select.go # SELECT clause
│ ├── from.go # FROM clause
│ ├── where.go # WHERE clause
│ ├── joins.go # JOIN clause
│ ├── order_by.go # ORDER BY clause
│ ├── group_by.go # GROUP BY + HAVING clause
│ ├── limit.go # LIMIT + OFFSET clause
│ ├── update.go # SET clause for UPDATE
│ ├── insert.go # INSERT INTO clause
│ ├── delete.go # DELETE clause
│ ├── values.go # VALUES clause
│ ├── set.go # SET assignment expressions
│ ├── on_conflict.go # ON CONFLICT / UPSERT clause
│ ├── returning.go # RETURNING clause (Postgres)
│ ├── locking.go # FOR UPDATE / FOR SHARE locking
│ ├── with.go # WITH (CTE) clause
│ └── association.go # Association join helpers
│
├── schema/ # Struct introspection and schema model
│ ├── schema.go # Schema struct — parsed representation of a Go model
│ ├── field.go # Field — maps struct field to DB column
│ ├── relationship.go # Relationship parsing (HasOne, HasMany, BelongsTo, M2M)
│ ├── constraint.go # FK/unique constraint definitions
│ ├── index.go # Index definitions from struct tags
│ ├── naming.go # NamingStrategy — table/column naming conventions
│ ├── serializer.go # Serializer interface (JSON, Gob, etc.)
│ ├── interfaces.go # Schema-internal interfaces
│ ├── pool.go # Schema cache pool
│ └── utils.go # Schema utilities
│
├── logger/ # Logging abstraction
│ ├── logger.go # Interface, Default logger, log levels, Config
│ ├── slog.go # slog-based logger implementation (Go 1.21+)
│ └── sql.go # SQL log formatting (redact vars, elapsed time)
│
├── migrator/ # Base migrator implementation
│ ├── migrator.go # CommonMigrator — shared migration logic
│ ├── column_type.go # ColumnType abstraction
│ ├── index.go # Index introspection helpers
│ └── table_type.go # TableType abstraction
│
├── internal/ # Private implementation details
│ ├── lru/lru.go # LRU cache (for prepared statement cache)
│ └── stmt_store/stmt_store.go # Thread-safe statement store
│
├── utils/ # Shared utility functions
│ ├── utils.go # String, reflect, and SQL utilities
│ └── tests/ # Test utility helpers
│
└── tests/ # Integration tests (56 test files, require a real DB)
├── compose.yml # Docker Compose for test databases
└── *_test.go # Feature-level integration testsEntry points#
GORM is a library — there are no binary entry points (cmd/ or main.go). The user-facing entry point is the exported function:
gorm.Open(dialector Dialector, opts ...Option) (*DB, error)ingorm.go— initializes a database session given a dialect (e.g.,sqlite.Open(...)) and optional config.
Package organization#
Internal packages:
internal/lru— LRU eviction cache used by the prepared statement systeminternal/stmt_store— thread-safe map for storing*sql.Stmthandles
Public packages (sub-packages):
gorm.io/gorm/callbacks— wires CRUD lifecycle hooks; implements create/query/update/delete callbacksgorm.io/gorm/clause— composable SQL AST (typed structs, not string concatenation); dialects extend thisgorm.io/gorm/schema— Go struct → database schema reflection via tags and conventionsgorm.io/gorm/logger— logging interface + default and slog implementationsgorm.io/gorm/migrator— portable migration logic shared by all dialect implementationsgorm.io/gorm/utils— low-level helpers (string manipulation, reflect)
Layering: The project follows a loose layered model:
- Root package (
gorm): User API surface, core types (DB,Config,Statement), interface contracts callbacks/: Depends on root — implements the CRUD pipeline by registering functions against the callback processorclause/: Dependency of root and callbacks — provides the SQL expression types used to build queriesschema/: Dependency of root and callbacks — provides struct reflection used to generate SQLlogger/,migrator/,utils/: Pure leaf packages, no import cycles
Crucially,
clause/andschema/have no dependency on the root package — a deliberate inversion that prevents import cycles and keeps these sub-systems independently testable.- Root package (
Build system#
- Build tool: Standard
go build/go test. No Makefile, no Goreleaser. - CI: GitHub Actions workflows:
tests.yml— runs integration tests (requires live databases via Docker Compose)golangci-lint.yml— static analysiscreate-release.yml— release automation- Other workflows: labeler, stale, invalid question (community hygiene)
- Key targets:
go test ./...(unit + integration); integration tests requirecompose.ymlto spin up databases - Docker: Only for testing (
tests/compose.ymlspins up Postgres, MySQL, etc.); no application container
Notable structural decisions#
Root package owns the API, sub-packages own the implementation details. The root
gormpackage exports all user-facing types (DB,Config,Model), while implementation-heavy domains (SQL AST, schema parsing) live in sub-packages. This keepsimport gorm.io/gormclean and self-contained.clause/is dialect-agnostic SQL IR. Instead of building SQL strings directly, every operation assembles typed clause structs. Dialects then render these viaClauseBuilderimplementations. This architectural choice enables the same query logic to target multiple databases without forking.callbacks/is the behavior extension point for both the library and users. All CRUD operations are implemented as registered callbacks (not hardcoded methods). Users and plugins can insert before/after hooks at named positions (BeforeCreate,AfterFind, etc.), making the callback pipeline the primary extension mechanism.No
vendor/, nocmd/, minimal dependencies. GORM deliberately avoids vendoring (consistent with library best practices) and keeps only 3 direct dependencies. Database drivers are external — this dramatically simplifies the core and lets each dialect evolve independently.tests/as a separate integration package with real databases. Integration tests live intests/and usegorm.io/driver/sqlite(or others via compose) rather than mocking. This makes the test suite slower but ensures real dialect behavior is covered. Unit tests exist within each sub-package (e.g.,clause/*_test.go,schema/*_test.go).