English
Gin framework oneof validation tag: pitfalls and best practices of single-quote notation

Problem site
After taking over a Gin framework project, testing feedback indicated that there was a problem with the query parameters of the list interface—the newly added notification type could not be correctly recognized. I looked at the backend API interface code:
go
type NotificationListRequest struct {
core.PaginationBaseRequest
Type string `json:"type" form:"type" binding:"oneof='email''sms''whatsapp''slack'''"`
}The moment I saw this line of code, my coding OCD kicked in—why oneof Should each value be wrapped in single quotes inside?
In my understanding, Gin's oneof The verification label should look like this:
go
// Official example style, clearly list all types.
Type string `binding:"oneof=email sms whatsapp slack dingding"`Spaces are concise, clear, and obvious at a glance. Yet this style wrapped in single quotes before us is baffling.
Project preparation
To illustrate this issue, I created a demo project Through two contrasting implementations, demonstrate the differences brought about by different writing styles. You can also directly view or download the source code and follow the steps below to verify.
Project Structure
├── README.md
├── cmd
│ ├── gin-binding-grace # Elegant implementation
│ │ └── main.go
│ └── gin-binding-oops # A problematic implementation
│ └── main.go
├── go.mod
├── go.sum
└── internal
└── pkg
└── core
└── response.go # Generic response structureGeneric response structure
internal/pkg/core/response.go Defined a unified API response format and pagination parameters:
go
package core
// PaginationBaseRequest defines common pagination request parameters.
type PaginationBaseRequest struct {
Number int `json:"number" form:"number,default=1"`
Size int `json:"size" form:"size,default=10"`
}
// PaginationResponse wraps paginated data
type PaginationResponse struct {
PaginationBaseRequest
Data any `json:"data"`
}
// Response Unified API response format
type Response struct {
Message string `json:"message"`
Code int `json:"code"`
Data any `json:"data,omitempty"`
}
// Business status code constants
const (
CodeWithSuccess = 100100
CodeWithFailed = 100101
)
// Success successful response
func Success(data interface{}) *Response {
return &Response{
Code: CodeWithSuccess,
Message: "success",
Data: data,
}
}
// Error error response
func Error(message string) *Response {
return &Response{
Code: CodeWithFailed,
Message: message,
}
}Bad API design (gin-binding-oops)
Specific implementation
go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/fishfinal/go-binding-demo/internal/pkg/core"
"github.com/gin-gonic/gin"
)
type NotificationListRequest struct {
core.PaginationBaseRequest
// Note this line: it is wrapped in single quotes and has no space separator.
Type string `json:"type" form:"type" binding:"oneof='email''sms''whatsapp''slack'''"`
}
func main() {
r := gin.Default()
v1 := r.Group("/api/v1")
{
v1.GET("/notifications", func(ctx *gin.Context) {
var parameters *NotificationListRequest
if err := ctx.ShouldBindQuery(¶meters); err != nil {
ctx.JSON(http.StatusBadRequest, core.Error(err.Error()))
return
}
marshal, _ := json.MarshalIndent(parameters, "", " ")
fmt.Println(string(marshal))
pagination := core.PaginationResponse{
PaginationBaseRequest: parameters.PaginationBaseRequest,
Data: []any{},
}
ctx.JSON(http.StatusOK, core.Success(pagination))
return
})
}
log.Fatal(r.Run(":8001"))
}Test results
bash
# Test various request scenarios
➜ curl "http://127.0.0.1:8001/api/v1/notifications?type="
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}
➜ curl "http://127.0.0.1:8001/api/v1/notifications?type"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}
➜ curl "http://127.0.0.1:8001/api/v1/notifications"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}
➜ curl "http://127.0.0.1:8001/api/v1/notifications?type=whatsapp"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}Server log output:
json
{
"number": 1,
"size": 10,
"type": ""
}
[GIN] 2026/07/08 - 00:02:11 | 200 | 65.478µs | 127.0.0.1 | GET "/api/v1/notifications?type="
{
"number": 1,
"size": 10,
"type": ""
}
[GIN] 2026/07/08 - 00:02:24 | 200 | 59.166µs | 127.0.0.1 | GET "/api/v1/notifications?type="
{
"number": 1,
"size": 10,
"type": ""
}
[GIN] 2026/07/08 - 00:02:27 | 200 | 68.184µs | 127.0.0.1 | GET "/api/v1/notifications?type"
{
"number": 1,
"size": 10,
"type": ""
}
[GIN] 2026/07/08 - 00:02:29 | 200 | 71.177µs | 127.0.0.1 | GET "/api/v1/notifications"
{
"number": 1,
"size": 10,
"type": "whatsapp"
}
[GIN] 2026/07/08 - 00:02:49 | 200 | 77.479µs | 127.0.0.1 | GET "/api/v1/notifications?type=whatsapp"Problem Analysis
All requests returned success, including cases where invalid values were passed in. Although oneof='email''sms''whatsapp''slack''' At runtime, it can be correctly parsed into 5 valid values, but the following issues exist:
Extremely poor readability: At first glance, it is completely impossible to identify which valid values there are.
easily misunderstood: Other developers might think this is a complete string.
Difficult to maintain: When adding a new type, I don’t know where it should be added or how to add it.
Unclear null value handling:
typeWhen empty,TypeIt is bound to an empty string, but the business semantics of "query all" are unclear.
The Truth About Single Quote Notation
The developer's original intent
write oneof='email''sms''whatsapp''slack''' At the time, the developer’s original intention was to express Type The field can support 5 valid values:
emailsmswhatsappslackempty string
''
Actual parsing behavior
In Go struct tags, adjacent string literals are automatically concatenated. So:
'email''sms''whatsapp''slack'''
↓ Adjacent string concatenation
'email' + '' + 'sms' + '' + 'whatsapp' + '' + 'slack' + ''Here's '' represents an empty string, so actually this tag Indeed represents 5 values, and can be correctly parsed at runtime.
So where is the problem?
The problem is not runtime parsing, but the readability and maintainability of the code!
This way of writing makes the code difficult to understand. When Developer A sees this code, it may take several minutes to understand that it represents 5 values. If it were written in the standard format, one could see at a glance which valid values there are.
Elegant API design (gin-binding-grace)
Specific implementation
go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/fishfinal/go-binding-demo/internal/pkg/core"
"github.com/gin-gonic/gin"
)
type NotificationListRequest struct {
core.PaginationBaseRequest
// Clear and explicit: use spaces as separators, set default values, and all types are clear at a glance.
Type string `json:"type" form:"type,default=all" binding:"oneof=all email sms whatsapp slack"`
}
func main() {
r := gin.Default()
v1 := r.Group("/api/v1")
{
v1.GET("/notifications", func(ctx *gin.Context) {
var parameters *NotificationListRequest
if err := ctx.ShouldBindQuery(¶meters); err != nil {
ctx.JSON(http.StatusBadRequest, core.Error(err.Error()))
return
}
marshal, _ := json.MarshalIndent(parameters, "", " ")
fmt.Println(string(marshal))
pagination := core.PaginationResponse{
PaginationBaseRequest: parameters.PaginationBaseRequest,
Data: []any{},
}
ctx.JSON(http.StatusOK, core.Success(pagination))
return
})
}
log.Fatal(r.Run(":8002"))
}Test results
bash
➜ curl "http://127.0.0.1:8002/api/v1/notifications?type=whatsapp"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}
➜ curl "http://127.0.0.1:8002/api/v1/notifications?type="
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}
➜ curl "http://127.0.0.1:8002/api/v1/notifications?type"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}
➜ curl "http://127.0.0.1:8002/api/v1/notifications"
{"message":"success","code":100100,"data":{"number":1,"size":10,"data":[]}}Server-side log output:
json
{
"number": 1,
"size": 10,
"type": "whatsapp"
}
[GIN] 2026/07/08 - 00:07:48 | 200 | 357.674µs | 127.0.0.1 | GET "/api/v1/notifications?type=whatsapp"
{
"number": 1,
"size": 10,
"type": "all"
}
[GIN] 2026/07/08 - 00:07:57 | 200 | 152.8µs | 127.0.0.1 | GET "/api/v1/notifications?type="
{
"number": 1,
"size": 10,
"type": "all"
}
[GIN] 2026/07/08 - 00:08:14 | 200 | 74.697µs | 127.0.0.1 | GET "/api/v1/notifications?type"
{
"number": 1,
"size": 10,
"type": "all"
}
[GIN] 2026/07/08 - 00:08:20 | 200 | 78.425µs | 127.0.0.1 | GET "/api/v1/notifications"Core advantages of elegant implementation
Clear enumeration values:
oneof=all email sms whatsapp slackClear at a glanceReasonable default values:
default=allGive null values a clear business meaningEasy to extend: Add
dingdingJust add it at the end.Self-documenting: The code itself is documentation
Comparison when adding the dingding type
Bad practice (continuing to pile onto the chaos)
go
// ❌ Continuing to concatenate, it becomes increasingly difficult to understand
Type string `binding:"oneof='email''sms''whatsapp''slack''dingding'''"`
// ❌ Added arbitrarily; not sure if it's correct
Type string `binding:"oneof='email''sms''whatsapp''slack'''dingding'"`An elegant approach (clear extension)
go
// ✅ Clearly list all types at a glance
Type string `json:"type" form:"type,default=all" binding:"oneof=all email sms whatsapp slack dingding"`Comparison of oneof tag syntax
| writing style | Analysis result | readability | Recommendation rating |
|---|---|---|---|
oneof='email''sms''whatsapp''slack''' | ["email", "sms", "whatsapp", "slack", ""] | ❌ Extremely poor | Not recommended |
oneof='email' 'sms' 'whatsapp' 'slack' '' | ["'email'", "'sms'", "'whatsapp'", "'slack'", "''"] | ⚠️ General | Not recommended |
oneof=email sms whatsapp slack | ["email", "sms", "whatsapp", "slack"] | ✅ Clear | Recommend |
oneof=all email sms whatsapp slack dingding | ["all", "email", "sms", "whatsapp", "slack", "dingding"] | ✅ Clear | Strongly recommend |
Best practices
1. Use standard notation
go
// ✅ Recommended: no quotes, space-separated
`binding:"oneof=email sms whatsapp slack dingding"`
// ✅ Recommended: Handle null values with default values
`form:"type,default=all" binding:"oneof=all email sms whatsapp slack dingding"`2. Use constants to manage enumeration values
go
const (
TypeAll = "all"
TypeEmail = "email"
TypeSMS = "sms"
TypeWhatsApp = "whatsapp"
TypeSlack = "slack"
TypeDingDing = "dingding"
)
type NotificationListRequest struct {
core.PaginationBaseRequest
Type string `json:"type" form:"type,default=all" binding:"oneof=all email sms whatsapp slack dingding"`
}3. Add code comments
go
type NotificationListRequest struct {
core.PaginationBaseRequest
// Type notification type filtering
// Supported: all, email, sms, whatsapp, slack, dingding
// Default: all (query all types)
Type string `json:"type" form:"type,default=all" binding:"oneof=all email sms whatsapp slack dingding"`
}Summary
Back to the original question:oneof='email''sms''whatsapp''slack''' This tag can indeed be correctly parsed at runtime into 5 valid values (email, sms, whatsapp, slack, empty string).
but Code is not only for machines to execute, but also for humans to read.. By comparing the two implementations, we can clearly see:
Readability is an important indicator of code quality.: Good code should let readers understand its intent at a glance.
Standards can prevent low-level mistakes. Unified writing standards can reduce comprehension and maintenance costs.
Details determine success or failure.: A difference in how a tag is written affects the collaboration efficiency of the entire team.
Good design is evolved.: from
gin-binding-oopstogin-binding-graceThe shift reflects a pursuit of code quality.
Therefore, we should choose a clearer and more standardized way of writing:
go
// Clear and concise, easy to understand at a glance.
binding:"oneof=all email sms whatsapp slack dingding"Every detail of the code deserves deep thought. Good code must not only run correctly, but also let readers quickly understand its intent. This is true "elegance."
Complete example code includes gin-binding-oops and gin-binding-grace Two comparative implementations, feel free to refer to and learn from.