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/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:
- "Marshal" the database to an
output.json
file. To "marshal" means to convert. I have no idea why people use "Marshal." - Open the
output.json
file 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.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:
json_data, err := json.MarshalIndent(choco, "", " ")
-- This takes thechoco
variable--which describes a chocolate cake--and converts it to raw JSON content. Thejson_data
holds the bytes you would write to a file.err = os.WriteFile("output.json", json_data, 0644)
-- Write thejson_data
to theoutput.json
file.again_data, err := os.ReadFile("output.json")
-- This is the "round-trip" part, where we readoutput.json
back.if !json.Valid(again_data)
-- Need to validate thatagain_data
is correct.err = json.Unmarshal(again_data, &new_cake)
-- Now do the inverse convert from bytes tonew_cake
struct.xml_out, err := xml.MarshalIndent(choco, "", " ")
-- As a last trick, output an XML version of the cake too. Notice there's no struct tags forxml
so this uses the `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
- 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 alteroutput.json
until things break when you try to validate it. - Change It -- Add
xml:
struct tags to get a different XML output. Add elements toCake
that 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
.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
- 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.