Video Coming Soon...
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/jsonis 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:
- "Marshal" the database to an
output.jsonfile. To "marshal" means to convert. I have no idea why people use "Marshal." - Open the
output.jsonfile for reading to read the raw bytes. - "Unmarshal" this raw JSON data back to recreate the original cake database.
- Then as an extra flex output that same data as XML with a similar
xml.MarshalIndentcall.
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:
json_data, err := json.MarshalIndent(choco, "", " ")-- This takes thechocovariable--which describes a chocolate cake--and converts it to raw JSON content. Thejson_dataholds the bytes you would write to a file.err = os.WriteFile("output.json", json_data, 0644)-- Write thejson_datato theoutput.jsonfile.again_data, err := os.ReadFile("output.json")-- This is the "round-trip" part, where we readoutput.jsonback.if !json.Valid(again_data)-- Need to validate thatagain_datais correct.err = json.Unmarshal(again_data, &new_cake)-- Now do the inverse convert from bytes tonew_cakestruct.xml_out, err := xml.MarshalIndent(choco, "", " ")-- As a last trick, output an XML version of the cake too. Notice there's no struct tags forxmlso this uses the `fmt.Println(string(xml_out))--Cake structnames 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
- Break It -- A great way to break this is to generate the
output.jsononce, then get rid of the code that generates and only input the file. Now just alteroutput.jsonuntil things break when you try to validate it. - Change It -- Add
xml:struct tags to get a different XML output. Add elements toCakethat you need for a recipe. There's so many ways you change this. - 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
.jsonfile, 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
- Read the documentation for Well known struct tags.
- Try some more conversions with the known struct tags and the packages they use.
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.