Database Patterns
February 28, 2026 · View on GitHub
SQL Database with go-zero
go-zero provides sqlx and sqlc packages for SQL operations with built-in connection pooling, caching, and resilience.
Basic SQL Operations Pattern
✅ Model Generation from SQL
# Generate model from existing database
goctl model mysql datasource \
-url="user:pass@tcp(localhost:3306)/database" \
-table="users" \
-dir="./model"
# Generate model from SQL DDL file
goctl model mysql ddl \
-src="./schema.sql" \
-dir="./model"
Example SQL Schema
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`email` varchar(255) NOT NULL UNIQUE,
`age` int NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_email` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
Generated Model Structure
// model/usersmodel.go
package model
import (
"context"
"database/sql"
"github.com/zeromicro/go-zero/core/stores/cache"
"github.com/zeromicro/go-zero/core/stores/sqlx"
)
var _ UsersModel = (*customUsersModel)(nil)
type (
// Interface for Users model operations
UsersModel interface {
usersModel
// Add custom methods here
}
customUsersModel struct {
*defaultUsersModel
}
// Generated struct
Users struct {
Id int64 `db:"id"`
Name string `db:"name"`
Email string `db:"email"`
Age int64 `db:"age"`
CreatedAt sql.NullTime `db:"created_at"`
UpdatedAt sql.NullTime `db:"updated_at"`
}
)
// NewUsersModel returns a model for Users
func NewUsersModel(conn sqlx.SqlConn, c cache.CacheConf) UsersModel {
return &customUsersModel{
defaultUsersModel: newUsersModel(conn, c),
}
}
// Generated methods (in usersmodel_gen.go):
// - Insert(ctx context.Context, data *Users) (sql.Result, error)
// - FindOne(ctx context.Context, id int64) (*Users, error)
// - FindOneByEmail(ctx context.Context, email string) (*Users, error)
// - Update(ctx context.Context, data *Users) error
// - Delete(ctx context.Context, id int64) error
CRUD Operations Pattern
✅ Insert
func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) (*types.CreateUserResponse, error) {
user := &model.Users{
Name: req.Name,
Email: req.Email,
Age: int64(req.Age),
}
result, err := l.svcCtx.UsersModel.Insert(l.ctx, user)
if err != nil {
l.Logger.Errorf("failed to insert user: %v", err)
return nil, err
}
userId, err := result.LastInsertId()
if err != nil {
return nil, err
}
return &types.CreateUserResponse{
Id: userId,
}, nil
}
✅ Find by Primary Key
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (*types.GetUserResponse, error) {
user, err := l.svcCtx.UsersModel.FindOne(l.ctx, req.Id)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.New("user not found")
}
return nil, err
}
return &types.GetUserResponse{
Id: user.Id,
Name: user.Name,
Email: user.Email,
Age: int(user.Age),
}, nil
}
✅ Find by Unique Index
func (l *GetUserByEmailLogic) GetUserByEmail(email string) (*model.Users, error) {
user, err := l.svcCtx.UsersModel.FindOneByEmail(l.ctx, email)
if err != nil {
if errors.Is(err, model.ErrNotFound) {
return nil, errors.New("user not found")
}
return nil, err
}
return user, nil
}
✅ Update
func (l *UpdateUserLogic) UpdateUser(req *types.UpdateUserRequest) error {
// Find existing user first
user, err := l.svcCtx.UsersModel.FindOne(l.ctx, req.Id)
if err != nil {
return err
}
// Update fields
if req.Name != "" {
user.Name = req.Name
}
if req.Age > 0 {
user.Age = int64(req.Age)
}
// Save changes
err = l.svcCtx.UsersModel.Update(l.ctx, user)
if err != nil {
l.Logger.Errorf("failed to update user: %v", err)
return err
}
return nil
}
✅ Delete
func (l *DeleteUserLogic) DeleteUser(req *types.DeleteUserRequest) error {
err := l.svcCtx.UsersModel.Delete(l.ctx, req.Id)
if err != nil {
l.Logger.Errorf("failed to delete user: %v", err)
return err
}
return nil
}
Custom Query Pattern
✅ Add Custom Methods to Model
// model/usersmodel.go
type (
UsersModel interface {
usersModel
// Custom methods
FindByAgeRange(ctx context.Context, minAge, maxAge int64) ([]*Users, error)
FindActiveUsers(ctx context.Context, limit int64) ([]*Users, error)
CountByAge(ctx context.Context, age int64) (int64, error)
}
customUsersModel struct {
*defaultUsersModel
}
)
func (m *customUsersModel) FindByAgeRange(ctx context.Context, minAge, maxAge int64) ([]*Users, error) {
query := `SELECT * FROM users WHERE age BETWEEN ? AND ? ORDER BY created_at DESC`
var users []*Users
err := m.QueryRowsNoCacheCtx(ctx, &users, query, minAge, maxAge)
if err != nil {
return nil, err
}
return users, nil
}
func (m *customUsersModel) FindActiveUsers(ctx context.Context, limit int64) ([]*Users, error) {
query := `SELECT * FROM users WHERE updated_at > DATE_SUB(NOW(), INTERVAL 30 DAY) LIMIT ?`
var users []*Users
err := m.QueryRowsNoCacheCtx(ctx, &users, query, limit)
if err != nil {
return nil, err
}
return users, nil
}
func (m *customUsersModel) CountByAge(ctx context.Context, age int64) (int64, error) {
query := `SELECT COUNT(*) FROM users WHERE age = ?`
var count int64
err := m.QueryRowNoCacheCtx(ctx, &count, query, age)
return count, err
}
✅ Pagination Pattern
func (m *customUsersModel) FindWithPagination(ctx context.Context, page, pageSize int64) ([]*Users, int64, error) {
// Get total count
var total int64
countQuery := `SELECT COUNT(*) FROM users`
err := m.QueryRowNoCacheCtx(ctx, &total, countQuery)
if err != nil {
return nil, 0, err
}
// Get paginated results
offset := (page - 1) * pageSize
query := `SELECT * FROM users ORDER BY id DESC LIMIT ? OFFSET ?`
var users []*Users
err = m.QueryRowsNoCacheCtx(ctx, &users, query, pageSize, offset)
if err != nil {
return nil, 0, err
}
return users, total, nil
}
Transaction Pattern
✅ Simple Transaction
func (l *TransferLogic) Transfer(from, to int64, amount float64) error {
// Start transaction
err := l.svcCtx.DB.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// Debit from account
debitQuery := `UPDATE accounts SET balance = balance - ? WHERE id = ? AND balance >= ?`
result, err := session.ExecCtx(ctx, debitQuery, amount, from, amount)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return errors.New("insufficient balance")
}
// Credit to account
creditQuery := `UPDATE accounts SET balance = balance + ? WHERE id = ?`
_, err = session.ExecCtx(ctx, creditQuery, amount, to)
if err != nil {
return err
}
// Record transaction
recordQuery := `INSERT INTO transactions(from_id, to_id, amount) VALUES(?, ?, ?)`
_, err = session.ExecCtx(ctx, recordQuery, from, to, amount)
return err
})
return err
}
✅ Complex Transaction with Multiple Models
func (l *CreateOrderLogic) CreateOrder(req *types.CreateOrderRequest) (*types.CreateOrderResponse, error) {
var orderId int64
err := l.svcCtx.DB.TransactCtx(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 1. Create order
orderQuery := `INSERT INTO orders(user_id, total_amount, status) VALUES(?, ?, ?)`
result, err := session.ExecCtx(ctx, orderQuery, req.UserId, req.TotalAmount, "pending")
if err != nil {
return fmt.Errorf("failed to create order: %w", err)
}
orderId, err = result.LastInsertId()
if err != nil {
return err
}
// 2. Create order items
itemQuery := `INSERT INTO order_items(order_id, product_id, quantity, price) VALUES(?, ?, ?, ?)`
for _, item := range req.Items {
_, err = session.ExecCtx(ctx, itemQuery, orderId, item.ProductId, item.Quantity, item.Price)
if err != nil {
return fmt.Errorf("failed to create order item: %w", err)
}
}
// 3. Update inventory
inventoryQuery := `UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?`
for _, item := range req.Items {
result, err = session.ExecCtx(ctx, inventoryQuery, item.Quantity, item.ProductId, item.Quantity)
if err != nil {
return fmt.Errorf("failed to update inventory: %w", err)
}
affected, _ := result.RowsAffected()
if affected == 0 {
return fmt.Errorf("insufficient stock for product %d", item.ProductId)
}
}
return nil
})
if err != nil {
l.Logger.Errorf("transaction failed: %v", err)
return nil, err
}
return &types.CreateOrderResponse{
OrderId: orderId,
}, nil
}
Caching Pattern
✅ Cache Configuration
# Configuration file
Cache:
- Host: localhost:6379
Type: node
Pass: "" # Redis password (optional)
# For Redis cluster
# - Host: localhost:6379,localhost:6380,localhost:6381
# Type: cluster
// Configuration struct
type Config struct {
rest.RestConf
DataSource string
Cache cache.CacheConf
}
✅ Model with Cache
When you use NewUsersModel(conn, c.Cache), caching is automatic for:
FindOne- Cached by primary keyFindOneByXxx- Cached by unique indexUpdate/Delete- Automatically invalidates cache
// Service context with cache
func NewServiceContext(c config.Config) *ServiceContext {
conn := sqlx.NewMysql(c.DataSource)
return &ServiceContext{
Config: c,
UsersModel: model.NewUsersModel(conn, c.Cache), // ✅ Cache enabled
}
}
✅ Custom Cache Keys
func (m *customUsersModel) FindByEmailWithCache(ctx context.Context, email string) (*Users, error) {
// Custom cache key
cacheKey := fmt.Sprintf("user:email:%s", email)
var user Users
err := m.QueryRowCtx(ctx, &user, cacheKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error {
query := `SELECT * FROM users WHERE email = ? LIMIT 1`
return conn.QueryRowCtx(ctx, v, query, email)
})
if err != nil {
return nil, err
}
return &user, nil
}
✅ Manual Cache Operations
// Get from cache
var user Users
err := m.CachedConn.GetCacheCtx(ctx, "user:123", &user)
// Set cache with expiration
err := m.CachedConn.SetCacheCtx(ctx, "user:123", user, time.Hour)
// Delete cache
err := m.CachedConn.DelCacheCtx(ctx, "user:123")
// Delete multiple cache keys
err := m.CachedConn.DelCacheCtx(ctx, "user:123", "user:email:test@test.com")
Connection Pooling Pattern
✅ Default Pool Configuration
go-zero uses sensible defaults:
// Default connection pool settings
MaxIdleConns: 64
MaxOpenConns: 64
ConnMaxLifetime: time.Minute
✅ Custom Pool Configuration
func NewServiceContext(c config.Config) *ServiceContext {
// Create connection with custom settings
conn := sqlx.NewMysql(c.DataSource)
// Customize pool (if needed)
db, err := conn.RawDB()
if err == nil {
db.SetMaxIdleConns(100)
db.SetMaxOpenConns(100)
db.SetConnMaxLifetime(time.Minute * 5)
}
return &ServiceContext{
Config: c,
UsersModel: model.NewUsersModel(conn, c.Cache),
}
}
Error Handling Pattern
✅ Handle Common Errors
import (
"github.com/zeromicro/go-zero/core/stores/sqlc"
)
func (l *GetUserLogic) GetUser(req *types.GetUserRequest) (*types.GetUserResponse, error) {
user, err := l.svcCtx.UsersModel.FindOne(l.ctx, req.Id)
if err != nil {
// Check for not found
if errors.Is(err, sqlc.ErrNotFound) {
return nil, errors.New("user not found")
}
// Check for database errors
if errors.Is(err, sql.ErrConnDone) {
l.Logger.Error("database connection error")
return nil, errors.New("database connection error")
}
// Generic error
l.Logger.Errorf("failed to find user: %v", err)
return nil, err
}
return &types.GetUserResponse{
Id: user.Id,
Name: user.Name,
Email: user.Email,
}, nil
}
✅ Handle Duplicate Key Errors
import (
"github.com/go-sql-driver/mysql"
)
func (l *CreateUserLogic) CreateUser(req *types.CreateUserRequest) (*types.CreateUserResponse, error) {
user := &model.Users{
Name: req.Name,
Email: req.Email,
}
result, err := l.svcCtx.UsersModel.Insert(l.ctx, user)
if err != nil {
// Check for duplicate key error
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
if mysqlErr.Number == 1062 { // Duplicate entry
return nil, errors.New("email already exists")
}
}
return nil, err
}
userId, _ := result.LastInsertId()
return &types.CreateUserResponse{Id: userId}, nil
}
MongoDB Pattern
✅ MongoDB Configuration
Mongo:
Host: localhost:27017
Type: mongo # or "mongos" for sharded cluster
User: username
Pass: password
Db: mydb
type Config struct {
rest.RestConf
Mongo struct {
Host string
Type string
User string `json:",optional"`
Pass string `json:",optional"`
Db string
}
}
✅ MongoDB Model
// model/usermodel.go
package model
import (
"context"
"github.com/zeromicro/go-zero/core/stores/mon"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/bson/primitive"
)
type User struct {
ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
Name string `bson:"name" json:"name"`
Email string `bson:"email" json:"email"`
Age int `bson:"age" json:"age"`
CreateAt int64 `bson:"create_at" json:"create_at"`
UpdateAt int64 `bson:"update_at" json:"update_at"`
}
type UserModel interface {
Insert(ctx context.Context, user *User) error
FindOne(ctx context.Context, id string) (*User, error)
FindOneByEmail(ctx context.Context, email string) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id string) error
}
type defaultUserModel struct {
conn *mon.Model
}
func NewUserModel(url, db, collection string) UserModel {
return &defaultUserModel{
conn: mon.MustNewModel(url, db, collection),
}
}
func (m *defaultUserModel) Insert(ctx context.Context, user *User) error {
user.ID = primitive.NewObjectID()
_, err := m.conn.InsertOne(ctx, user)
return err
}
func (m *defaultUserModel) FindOne(ctx context.Context, id string) (*User, error) {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return nil, err
}
var user User
err = m.conn.FindOne(ctx, &user, bson.M{"_id": oid})
return &user, err
}
func (m *defaultUserModel) FindOneByEmail(ctx context.Context, email string) (*User, error) {
var user User
err := m.conn.FindOne(ctx, &user, bson.M{"email": email})
return &user, err
}
func (m *defaultUserModel) Update(ctx context.Context, user *User) error {
oid, err := primitive.ObjectIDFromHex(user.ID.Hex())
if err != nil {
return err
}
_, err = m.conn.UpdateOne(ctx, bson.M{"_id": oid}, bson.M{"$set": user})
return err
}
func (m *defaultUserModel) Delete(ctx context.Context, id string) error {
oid, err := primitive.ObjectIDFromHex(id)
if err != nil {
return err
}
_, err = m.conn.DeleteOne(ctx, bson.M{"_id": oid})
return err
}
Best Practices Summary
✅ DO:
- Use
goctlto generate models from database schema - Always pass
context.Contextto database operations - Use transactions for operations that must be atomic
- Enable caching for read-heavy models
- Handle
sqlc.ErrNotFoundexplicitly - Use connection pooling (automatic by default)
- Add custom methods to model interface
- Log database errors with context
- Use parameterized queries (automatic with go-zero)
- Validate data before database operations
❌ DON'T:
- Execute raw SQL without parameterization
- Ignore errors from database operations
- Use
_to discard errors - Create database connections in handlers/logic
- Keep transactions open longer than necessary
- Query in loops (use batch operations)
- Store sensitive data unencrypted
- Use
SELECT *in production code (be explicit) - Cache write-heavy data unnecessarily
- Forget to close result sets/cursors
When to Use Each Database Type
MySQL/PostgreSQL (SQL):
- Structured data with relationships
- ACID transactions required
- Complex queries with JOINs
- Strong consistency needed
- Traditional CRUD operations
MongoDB:
- Flexible schema
- Horizontal scaling
- Document-oriented data
- High write throughput
- Hierarchical data
Redis (Cache):
- Session storage
- Rate limiting
- Real-time leaderboards
- Pub/sub messaging
- Hot data caching
For Redis-specific patterns, see Resilience Patterns.