Video Coming Soon...

Created by Zed A. Shaw Updated 2025-10-07 14:32:54

25: The Amazing Struct Tags

WARNING This exercise is in DRAFT status, so there may be errors. If you find any, please email me at help@learncodethehardway.com so I can fix them.

I think that Go's struct tags are probably one of the best programming language features I've seen in a long time. With struct tags you can augment or "tag" the fields of a struct to tell Go how to process those fields in different situations. Here's an example for us to talk about:

type Person struct {
    Name `db:"name" json:"name" validate:"required,max=30"`
    Age `db:"name" json:"name" validate:"required"`
}

In this code I'm creating a Person struct like normal, with Name and Age fields. Go isn't doing anything different until I use this struct in a Database API, JSON API, or an HTML Form situation. If I store Person to a SQL database then the SQL package I use can look at the db:"name" and it knows to change Person.Name to name in the SQL. If I output this same Person to JSON then the encoding/json package knows to change the Person.Name to name in the .json file.

But, simply changing names isn't the only thing you can do with struct tags. The validate:"required" for Person.Age is used by an HTML form validation package that will take a Person and validate the form input based on that rule. Need to make sure names are a certain length, or that an email is actually an email? Then you can add validate: rules to it.

This simple feature turns Go into a very good data processing and conversion tool. If you need to convert from a SQL database to a JSON file then it's very easy, and the SQL database can have truly disgusting field names that will be "automatically" translated to nicer ones in the JSON (if you want).

The Code

NOTE A new version of encoding/json is in experimental https://pkg.go.dev/encoding/json/v2 and that might invalidate the code in this exercise.

Let's round-trip a Cakeshop database to and from JSON as an example. The term "round-trip" means to input some data, convert it, output it, then repeat the inverse with the output. In this code below we'll do it like this:

  1. "Marshal" the database to an output.json file. To "marshal" means to convert. I have no idea why people use "Marshal."
  2. Open the output.json file for reading to read the raw bytes.
  3. "Unmarshal" this raw JSON data back to recreate the original cake database.
  4. Then as an extra flex output that same data as XML with a similar xml.MarshalIndent call.

View Source file ex25/main.go Only

package main

import (
    "encoding/json"
    "encoding/xml"
    "fmt"
    "log"
    "os"
)

type Ingredient struct {
    Name   string
    Amount int
}

type Money struct {
    Dollars int `json:"dollars"`
    Cents   int `json:"cents"`
}

type Cake struct {
    Name        string       `json:"name"`
    Ingredients []Ingredient `json:"ingredients"`
    Description string       `json:"description"`
    Price       Money        `json:"price"`
}

func main() {
    choco := Cake{
        "Chocolate",
        []Ingredient{
            Ingredient{"Chocolate", 1},
            Ingredient{"Butter", 2},
            Ingredient{"Flour", 2},
            Ingredient{"Eggs", 4},
        },
        "A really good chocolate cake.",
        Money{30, 99},
    }

    json_data, err := json.MarshalIndent(choco, "", "  ")
    if err != nil { log.Fatal(err) }

    err = os.WriteFile("output.json", json_data, 0644)
    if err != nil { log.Fatal(err) }

    again_data, err := os.ReadFile("output.json")
    if err != nil { log.Fatal(err) }

    if !json.Valid(again_data) {
        log.Fatal("invalid json data")
    }

    var new_cake Cake
    err = json.Unmarshal(again_data, &new_cake)
    if err != nil { log.Fatal(err) }

    fmt.Println(new_cake)

    xml_out, err := xml.MarshalIndent(choco, "", "  ")
    if err != nil { log.Fatal(err) }

    // this will output raw, try adding xml:"" tags like json
    fmt.Println(string(xml_out))
}

The Breakdown

The important lines in this code are:

  1. json_data, err := json.MarshalIndent(choco, "", " ") -- This takes the choco variable--which describes a chocolate cake--and converts it to raw JSON content. The json_data holds the bytes you would write to a file.
  2. err = os.WriteFile("output.json", json_data, 0644) -- Write the json_data to the output.json file.
  3. again_data, err := os.ReadFile("output.json") -- This is the "round-trip" part, where we read output.json back.
  4. if !json.Valid(again_data) -- Need to validate that again_data is correct.
  5. err = json.Unmarshal(again_data, &new_cake) -- Now do the inverse convert from bytes to new_cake struct.
  6. xml_out, err := xml.MarshalIndent(choco, "", " ") -- As a last trick, output an XML version of the cake too. Notice there's no struct tags for xml so this uses the `
  7. fmt.Println(string(xml_out)) -- Cake struct names directly. This is usually fine to do if you don't have to interface with anything expecting a certain naming format.

When you're done running this you should go look at the output.json file and see that it contains the expected field name.

The Practice

  1. Break It -- A great way to break this is to generate the output.json once, then get rid of the code that generates and only input the file. Now just alter output.json until things break when you try to validate it.
  2. Change It -- Add xml: struct tags to get a different XML output. Add elements to Cake that you need for a recipe. There's so many ways you change this.
  3. Recreate It -- Instead of recreating this from memory I'd suggest coming up with your own data you need/want to store and read. Design a first .json file, then write the code to load it and work on it from there. Maybe you create a little TODO App or Notes App?

Study Drills

  1. Read the documentation for Well known struct tags.
  2. Try some more conversions with the known struct tags and the packages they use.
Previous Lesson Next Lesson

Register for Learn Go the Hard Way

Register today for the course and get the all currently available videos and lessons, plus all future modules for no extra charge.