Build Your Own Go ORM: Logger, Struct Mapping, CRUD, Transactions & Hooks
This tutorial walks through creating a lightweight Go ORM—covering a custom logger, converting structs to table metadata, implementing basic CRUD operations, handling transactions, and adding hook callbacks—while exposing the core Session data structure and providing a GitHub reference.
Project is a stripped‑down version of GORM that demonstrates the internal behavior of each ORM function.
Repository: https://github.com/gofish2020/easyorm
Core Data Structure
The central type is Session:
type Session struct {
db *sql.DB
tx *sql.Tx
// table
tableMeta *table.TableMeta
dial dialect.Dialect
// clause
clause clause.Clause
// sql query
sql strings.Builder
// sql param
sqlParam []interface{}
}db : executes SQL statements.
tx : executes SQL within a transaction.
tableMeta : metadata derived from a Go struct.
clause : builds CRUD SQL strings.
dial : converts Go types to database types.
sql / sqlParam : the SQL statement and its parameters ready for execution.
Custom Logger
The logger prints messages in different colors according to log level.
Struct‑to‑Table Metadata
// Parse converts a struct into table/field information
func Parse(dest interface{}, d dialect.Dialect) *TableMeta {
if dest == nil {
return nil
}
value := reflect.ValueOf(dest)
if value.Kind() == reflect.Ptr && value.IsNil() {
value = reflect.New(value.Type().Elem()) // create a new instance
}
meta := &TableMeta{
Model: value.Interface(),
fieldMap: make(map[string]*Field),
}
destValue := reflect.Indirect(value)
destType := destValue.Type()
if m, ok := value.Interface().(Tabler); ok {
meta.TableName = m.TableName()
} else {
meta.TableName = destType.Name()
}
for i := 0; i < destType.NumField(); i++ {
fieldType := destType.Field(i)
if !fieldType.Anonymous && fieldType.IsExported() {
field := &Field{
FieldName: fieldType.Name,
FieldType: d.ConvertType2DBType(destValue.Field(i)),
FieldTag: fieldType.Tag.Get("easyorm"),
}
meta.Fields = append(meta.Fields, field)
meta.FieldsName = append(meta.FieldsName, field.FieldName)
meta.fieldMap[field.FieldName] = field
}
}
return meta
}CRUD – Insert Example
Typical call:
s.Insert(&User{}, &User{}) func (s *Session) Insert(values ...interface{}) (int64, error) {
if len(values) == 0 {
return 0, errors.New("param is empty")
}
// init table
s.Model(values[0])
// clause insert: generate "INSERT INTO $table ($fields)"
s.clause.Set(clause.INSERT, s.TableMeta().TableName, s.TableMeta().FieldsName)
valueSlice := []interface{}{}
for _, value := range values {
s.CallMethod(BeforeInsert, value)
// extract field values as SQL parameters
valueSlice = append(valueSlice, s.TableMeta().ExtractFieldValue(value))
}
// clause values: generate "VALUES (?,?,?),(?,?,?)"
s.clause.Set(clause.VALUES, valueSlice...)
// build final SQL
sql, sqlParam := s.clause.Build(clause.INSERT, clause.VALUES)
// execute
result, err := s.Raw(sql, sqlParam...).Exec()
if err != nil {
return 0, err
}
s.CallMethod(AfterInsert, nil)
return result.RowsAffected()
}Transaction Support
// TxFunc will be called between tx.Begin() and tx.Commit()
type TxFunc func(*session.Session) (interface{}, error)
// Transaction executes SQL wrapped in a transaction and automatically commits if no error occurs
func (engine *Engine) Transaction(f TxFunc) (result interface{}, err error) {
s := engine.NewSession()
if err = s.Begin(); err != nil {
return nil, err
}
defer func() {
if p := recover(); p != nil {
_ = s.Rollback()
panic(p)
} else if err != nil {
_ = s.Rollback()
} else {
err = s.Commit()
}
}()
return f(s)
}Hook Mechanism
Hooks are invoked via reflect.MethodByName and Call, allowing user‑defined methods to run before or after specific operations.
func (s *Session) CallMethod(method string, value interface{}) {
fm := reflect.ValueOf(s.TableMeta().Model).MethodByName(method)
if value != nil {
fm = reflect.ValueOf(value).MethodByName(method)
}
param := []reflect.Value{reflect.ValueOf(s)}
if fm.IsValid() {
if v := fm.Call(param); len(v) > 0 {
if err, ok := v[0].Interface().(error); ok {
logger.Error(err)
}
}
}
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Nullbody Notes
Go backend development, learning open-source project source code together, focusing on simplicity and practicality.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
