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.
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.
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.
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.
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.
