How Gin + Go Generics Eliminate Copy‑Paste in CRUD Handlers

The article shows how repetitive CRUD code in Gin can be refactored with Go 1.18 generics by defining a Creatable interface, writing a single generic CreateHandler, using factory functions and type constraints, and registering routes in one line, resulting in zero duplicate code and clearer responsibilities.

Golang Shines
Golang Shines
Golang Shines
How Gin + Go Generics Eliminate Copy‑Paste in CRUD Handlers

Story: The Copy‑Paste Nightmare

Late at night engineer Xiao Ming copies the CreateUser function for the tenth time, only to change the type name to CreateProduct and realize the code looks identical to the previous one. The arrival of Go 1.18 generics promises a way out of this pain.

Before Generics: Repetitive CRUD Handlers

Without generics each resource requires its own handler, leading to duplicated code for binding JSON, error handling, database creation and response.

// Create user
func CreateUser(c *gin.Context) {
    var user User
    if err := c.ShouldBindJSON(&user); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    if err := db.Create(&user).Error; err != nil {
        c.JSON(500, gin.H{"error": "create failed"})
        return
    }
    c.JSON(201, user)
}

// Create product (copy the above, change User→Product)
func CreateProduct(c *gin.Context) {
    var product Product // 👈 only changed here
    if err := c.ShouldBindJSON(&product); err != nil {
        c.JSON(400, gin.H{"error": err.Error()})
        return
    }
    if err := db.Create(&product).Error; err != nil {
        c.JSON(500, gin.H{"error": "create failed"})
        return
    }
    c.JSON(201, product)
}

Introducing Generics in Go 1.18

Define an interface that marks any type that can be created.

// Any type that wants to be "creatable" must implement this interface
type Creatable interface {}

Write a generic handler that works for any T satisfying Creatable.

func CreateHandler[T Creatable](c *gin.Context) gin.HandlerFunc {
    return func(c *gin.Context) {
        var entity T
        if err := c.ShouldBindJSON(&entity); err != nil {
            c.JSON(400, gin.H{"error": "invalid params"})
            return
        }
        if err := db.Create(&entity).Error; err != nil {
            c.JSON(500, gin.H{"error": "create failed"})
            return
        }
        c.JSON(201, entity)
    }
}

Register routes with a single line per resource.

// Traditional registration (one function per resource)
router.POST("/users", CreateUser)
router.POST("/products", CreateProduct)
router.POST("/orders", CreateOrder)

// Generic registration (one line per resource)
router.POST("/users", CreateHandler[User])
router.POST("/products", CreateHandler[Product])
router.POST("/orders", CreateHandler[Order])

Advanced Techniques

Factory Functions for Instantiation

Because the concrete type T is only known at compile time, a factory function is needed to create a zero‑value instance.

func CreateHandler[T Creatable](factory func() T, c *gin.Context) gin.HandlerFunc {
    return func(c *gin.Context) {
        entity := factory() // ✅ use factory to instantiate
        if err := c.ShouldBindJSON(&entity); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        // ... create logic ...
        c.JSON(201, entity)
    }
}

// Usage example
router.POST("/users", CreateHandler(func() User { return User{} }))
router.POST("/products", CreateHandler(func() Product { return Product{} }))

Type Constraints for Validation

Define a richer constraint that requires an ID field and a Validate method.

type Resource interface {
    Creatable
    GetID() uint
    Validate() error // custom validation logic
}

func CreateHandler[T Resource](factory func() T, c *gin.Context) gin.HandlerFunc {
    return func(c *gin.Context) {
        entity := factory()
        if err := c.ShouldBindJSON(&entity); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        if err := entity.Validate(); err != nil {
            c.JSON(400, gin.H{"error": err.Error()})
            return
        }
        // ... create logic ...
        c.JSON(201, entity)
    }
}

Unified Response Structure

Define a generic response wrapper so the front‑end always receives {code, message, data}.

type Response[T any] struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
    Data    T      `json:"data,omitempty"`
}

func Success[T any](c *gin.Context, data T) {
    c.JSON(200, Response[T]{Code: 200, Message: "success", Data: data})
}

func Fail(c *gin.Context, err error) {
    c.JSON(400, Response[any]{Code: 400, Message: err.Error()})
}

Use Success and Fail inside the generic handler to keep the response format consistent.

Full Example: From Zero to a Generic CRUD

Define the resource constraint, a generic Create handler, a generic GetByID handler, and register routes with a single line each.

type Resource interface {
    GetID() uint
    TableName() string // required by GORM
}

func Create[T Resource](factory func() T, c *gin.Context) gin.HandlerFunc { /* same logic as above */ }

func GetByID[T Resource](factory func() T, c *gin.Context) gin.HandlerFunc {
    return func(c *gin.Context) {
        id := c.Param("id")
        entity := factory()
        if err := db.Where("id = ?", id).First(&entity).Error; err != nil {
            c.JSON(404, gin.H{"error": "not found"})
            return
        }
        c.JSON(200, entity)
    }
}

func RegisterUserRoutes(r *gin.RouterGroup) {
    r.POST("/users", Create(func() User { return User{} }))
    r.GET("/users/:id", GetByID(func() User { return User{} }))
    // Update/Delete follow the same pattern
}

The result is a concise, reusable CRUD layer where adding a new resource only requires defining its struct and a one‑line route registration.

Key Takeaways

If you are still writing copy‑paste CRUD code, try Go generics to free your hands.

Start with the CreateHandler function and gradually refactor other operations.

Clear generic constraints make the codebase approachable for newcomers during code reviews.

Generic code is not a gimmick; it abstracts repetitive patterns so developers can focus on business differences.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Backend DevelopmentgoGenericsCRUDCode ReuseGORMgin
Golang Shines
Written by

Golang Shines

We share daily the latest Golang technical articles, practical resources, language news, tutorials, and real-world projects to help everyone learn and improve.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.