Golang Technique: Custom Struct Tag Like `json:"name"`
Do you know the json:"name" tag in Go? You can actually create your own custom struct tags, just like it. This is a great way to make your code more flexible and easier to work with.
In Go, you can attach additional metadata to struct fields via struct tags.
type A struct {
Name int `tag:"thisistag"` // <----
}
These tags come in handy for various tasks, such as designating field names when you’re converting a struct to or from formats like JSON or XML. Plus, they give you advanced features like the ‘omitempty
’ option.
With the help of reflection, you can tap into these struct tags to define how your code behaves.
“What’s this reflection stuff anyway?”
I‘ve penned an article that covers all you need to get going with it, feel free to check it out here Reflection in Go: Everything You Need to Know to Use It Effectively.
Let’s dive in.
1. Explain Validation Sample
Let’s start by examining the sample code below to grasp what we’ll be discussing in this piece:
type Student struct {
Age int `validate:"min=18"`
Name string `validate:"required"`
}
func main() {
s := Student{Age: 90, Name: "John"}
err := Validate(s)
if err != nil {
fmt.Println(err)
}
}
In the code, we’ve defined a struct named Student
Within it, there’s a field labeled Age
that carries a struct tag min=18
, setting the lower age limit at 18. Another field is Name
, includes a struct tag validate:"required"
which specifies that this field can't be left blank.
So what’s the aim of this article?
We’ll focus on crafting a function named Validate(any)
capable of interpreting these struct tags. It will determine whether the student's age is at least 18 and ensure the name field is filled in.
2. Understanding Validation Through an Example
First off, to operate on the
Student
struct, we need to turn it into areflect.Value
object.
If you're scratching your head on that, you may want to revisit my earlier article about reflection.
// create a student which violates age validation
student := Student{Age: 17, Name: "Aiden"}
func Validate(s interface{}) {
// get the value of interface{}/ pointer point to
val := reflect.Indirect(reflect.ValueOf(s))
}
To transform the student struct into a reflect.Value
object, we apply reflect.ValueOf(student)
.
Now, given we can't be sure if the incoming argument s
is a pointer, an interface, or just a plain struct, we pull in reflect.Indirect()
to get the real value of the struct.
2. Next up, we need to run through every field in the Student
struct to grab what’s in the validate
struct tag.
for i := 0; i < val.NumField(); i++ {
typeField := val.Type().Field(i) // get field i-th of type(val)
tag := typeField.Tag.Get("validate")
if tag == "" {
continue
}
fmt.Println(tag)
}
// min=18
// required
3. Now that we have the validation rules at hand, all that’s left is to implement them accordingly:
// get the value of field like 17 or "Aiden"
valueField := val.Field(i)
// split the tag so we can use like this: `required:"limit=20"
rules := strings.Split(tag, ",")
for _, rule := range rules {
parts := strings.Split(rule, "=")
key := parts[0]
var value string
if len(parts) > 1 {
value = parts[1]
}
switch key {
case "required":
if err := isRequired(valueField); err != nil {
return err
}
case "min":
if err := isMin(valueField, value); err != nil {
return err
}
}
}
4. (Optional) You want to know how the isMin
and isRequired
validation rules actually work? Keep reading.
Here’s how we deal with
isMin
validation:
func isMin(valueField reflect.Value, minStr string) error {
typeField := valueField.Type()
if minStr == "" {
return nil
}
min, err := strconv.ParseFloat(minStr, 64)
if err != nil {
return fmt.Errorf("min value %f is not a number", min)
}
switch valueField.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if float64(valueField.Int()) < min {
return fmt.Errorf("field %s must be greater or equal %d", typeField.Name(), int(min))
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if float64(valueField.Uint()) < min {
return fmt.Errorf("field %s must be greater or equal than %d", typeField.Name(), uint(min))
}
case reflect.Float32, reflect.Float64:
if valueField.Float() < min {
return fmt.Errorf("field %s must be greater or equal than %f", typeField.Name(), min)
}
}
return nil
}
Here’s what’s going on for
isRequired
:
func isRequired(v reflect.Value) error {
switch v.Kind() {
case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
if v.Len() != 0 {
return nil
}
case reflect.Bool:
if v.Bool() {
return nil
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if v.Int() != 0 {
return nil
}
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if v.Uint() != 0 {
return nil
}
}
return fmt.Errorf("field %s is required", v.Type().Name())
}
That’s the core logic behind our validation checks.
3. Using package
This piece uses validation to illustrate how custom struct tags work, but let’s be real, do you always have to make everything from scratch?
“Are custom struct tags just a fancy trick?”
No, they’re genuinely useful, as shown here.
Yet, when you’re dealing specifically with validation, maybe don’t go out of your way to build it yourself. Existing validation libraries usually offer broader features and manage edge cases more effectively.
Take the “validator” package as a case in point. It comes with a host of ready-to-use functions like ‘required
’ and ‘min
,’ making the validation much easier than if you started from zero.
If you’re looking for the complete code, it’s stored in my gist “Custom struct tag in Go”