13 KiB
pulley
A Golang HTTP API
Routes
Uses CleanURLs
Method | URL Pattern | Handler | Action |
---|---|---|---|
GET | /v1/healthcheck | healthcheckHandler | Show application information |
GET | /v1/movies | listMoviesHandler | Show the details of all movies |
POST | /v1/movies | createMovieHandler | Create a new movie |
GET | /v1/movies/:id | showMovieHandler | Show the details of a specific movie |
PUT | /v1/movies/:id | editMovieHandler | Update the details of a specific movie |
DELETE | /v1/movies/:id | deleteMovieHandler | Delete a specific movie |
Go JSON Mapping Reference
Go type | JSON type |
---|---|
bool | JSON boolean |
string | JSON string |
int*, uint*, float*, rune | JSON number |
arrays, non-nil slices | JSON array |
structs, non-nil maps | JSON object |
nil pointers, interface values, slices, maps | JSON null |
chan, func, complex* | Not supported |
time.Time | RFC3339-format JSON string |
[]byte | Base64-encoded JSON string |
- Go
time.Time
values (which are actually a struct behind the scenes) will be encoded as a JSON string in RFC 3339 format like "2020-11-08T06:27:59+01:00", rather than as a JSON object. - A
[]byte
slice will be encoded as a base64-encoded JSON string, rather than as a JSON array. So, for example, a byte slice of[]byte{'h','e','l','l','o'}
would appear as "aGVsbG8=" in the JSON output. The base64 encoding uses padding and the standard character set. - Encoding of nested objects is supported. So, for example, if you have a slice of structs in Go that will encode to an array of objects in JSON.
- Channels, functions and complex number types cannot be encoded. If you try to do so, you’ll get a json.
UnsupportedTypeError
error at runtime. - Any pointer values will encode as the value pointed to.
- Struct fields MUST be exported for them to be serialize
- If a struct field doesn’t have an explicit value set, then the JSON-encoding of the zero value for the field will appear in the output.
- It’s also possible to control the visibility of individual struct fields in the JSON by using the
omitzero
and-
struct tag directives.- The
-
(hyphen) directive can be used when you never want a particular struct field to appear in the JSON output. This is useful for fields that contain internal system information that isn’t relevant to your users, or sensitive information that you don’t want to expose (like the hash of a password) - In contrast, the omitzero directive hides a field in the JSON output if and only if the value is the zero value for the field type. If you want to use omitzero and not change the key name then you can leave it blank in the struct tag — like this:
json:",omitzero"
. Notice that the leading comma is still required.
- The
- An older
omiteby creating a custom envelope map with the underlying type map[string]any.mpty
struct field( Go <= 1.23) has some "stranger" behaviors that overlap with omitzero.omitempty
will not omit structs, even if all the struct fields have their zero value.omitempty
will not omit time.Time types, even if they have their zero value. Note that this is because the time.Time type is actually a struct behind-the-scenes, so this is really just a special case of the bullet point above.omitempty
will not omit arrays, even if they have their zero value.omitempty
will omit empty slices and maps (that is, initialized slices and maps with length zero) as well as nil slices and maps.
- A final, less-frequently-used, struct tag directive is
string
. You can use this on individual struct fields to force the data to be represented as a string in the JSON output.- Note that the string directive will only work on struct fields which have
int*
,uint*
,float*
orbool
types. For any other type of struct field it will have no effect.
- Note that the string directive will only work on struct fields which have
- Check out the [appendix chapter](file:///home/toor/Desktop/lets-go-further-v1.24.1/lets-go-further.html/21. 04-json-encoding-nuances.html) for additional nuances on encoding types.
The pattern err := json.NewEncoder(w).Encode(data)
looks elegant but creates issues when needing to coditionally set headers in response to an error. The performance difference between json.Marshal
and json.Encoder
are so small(tiny more mem and 1 extra alloc) that it's just cleaner to use json.Marshal
.
How Go Marshals JSON
When Go is encoding a particular type to JSON, it looks to see if the type has a MarshalJSON() method implemented on it. If it has, then Go will call this method to determine how to encode it.
Here is the interface
type Marshaler interface {
MarshalJSON() ([]byte, error)
}
If the type doesn’t have a MarshalJSON() method, then Go will fall back to trying to encode it to JSON based on its own internal set of rules.
Enveloping Respsonses
{
"movie": {
"id": 123,
"title": "Casablanca",
"runtime": 102,
"genres": [
"drama",
"romance",
"war"
],
"version":1
}
}
Enveloping response data like this isn’t strictly necessary, and whether you choose to do so is partly a matter of style and taste. But there are a few tangible benefits:
- Including a key name (like "movie") at the top-level of the JSON helps make the response more self-documenting. For any humans who see the response out of context, it is a bit easier to understand what the data relates to.
- It reduces the risk of errors on the client side, because it’s harder to accidentally process one response thinking that it is something different. To get at the data, a client must explicitly reference it via the "movie" key.
- If we always envelope the data returned by our API, then we mitigate a security vulnerability in older browsers which can arise if you return a JSON array as a response.
Decoding JSON
The json.Unmarshal()
is an alternative, but less performant and more verbose method of decoding json compared to json.NewDecoder().Decode()
. It is preferable to use the Decode
implementation.
var myanonymous struct {
Title string `json:"title`
Year int32 `json:"year"`
}
err := json.NewDecoder(r.Body).Decode(&myanonymous)
- When calling Decode() you must pass a non-nil pointer as the target decode destination. If you don’t use a pointer, it will return a json.InvalidUnmarshalError error at runtime.
- If the target decode destination is a struct — like in our case — the struct fields must be exported (start with a capital letter). Just like with encoding, they need to be exported so that they’re visible to the encoding/json package.
- When decoding a JSON object into a struct, the key/value pairs in the JSON are mapped to the struct fields based on the struct tag names. If there is no matching struct tag, Go will attempt to decode the value into a field that matches the key name (exact matches are preferred, but it will fall back to a case-insensitive match). Any JSON key/value pairs which cannot be successfully mapped to the struct fields will be silently ignored.
- There is no need to close r.Body after it has been read. This will be done automatically by Go’s http.Server, so you don’t have to.
Go decoding mapping reference
Go type | JSON type |
---|---|
bool | JSON boolean |
string | JSON string |
int*, uint*, float*, rune | JSON number |
arrays, non-nil slices | JSON array |
structs, non-nil maps | JSON object |
nil pointers, (interface values, slices, maps) | JSON null |
A note on null and decoding. If the struct we are decoding to contains pointers, then the value of null will be mapped to a nil pointer. In a non-pointer struct the value will be the zero value of the type.
type Person struct {
Name *string `json:"name"` // nil if null
Age *int `json:"age"` // nil if null
}
vs
type Person struct {
Name string `json:"name"` // "" if null
Age int `json:"age"` // 0 if null
}
JSON Decoding Nuances
Decoding into Go arrays
When you’re decoding a JSON array into a Go array (not a slice) there are a couple of important behaviors to be aware of:
- If the Go array is smaller than the JSON array, then the additional JSON array elements are silently discarded.
- If the Go array is larger than the JSON array, then the additional Go array elements are set to their zero values.
Partial JSON decoding
If you have a lot of JSON input to process and only need a small part of it, it’s often possible to leverage the json.RawMessage type to help deal with this. For example:
// Let's say that the only thing we're interested in is processing the "genres" array in
// the following JSON object
js := `{"title": "Top Gun", "genres": ["action", "romance"], "year": 1986}`
// Decode the JSON object to a map[string]json.RawMessage type. The json.RawMessage
// values in the map will retain their original, un-decoded, JSON values.
var m map[string]json.RawMessage
err := json.NewDecoder(strings.NewReader(js)).Decode(&m)
if err != nil {
log.Fatal(err)
}
// We can then access the JSON "genres" value from the map and decode it as normal using
// the json.Unmarshal() function.
var genres []string
err = json.Unmarshal(m["genres"], &genres)
if err != nil {
log.Fatal(err)
}
fmt.Printf("genres: %v\n", genres)
Decoding into any types
It’s possible to decode JSON values into an any
type. When you do this, the underlying value that the any
type holds will depend on the type of the JSON value being decoded.
JSON type | Go type |
---|---|
JSON boolean | bool |
JSON string | string |
JSON number | float64 |
JSON array | []any |
JSON object | map[string]any |
JSON null | nil |
Decoding into an any type can be useful in situations where:
- You don’t know in advance exactly what you’re decoding.
- You need to decode JSON arrays which contain items with different JSON types.
- The key/value pair in a JSON object doesn’t always contain values with the same JSON type.
When you decode a JSON number into an any type the value will have the underlying type float64
— even if it is an integer in the original JSON. If you want to get the value as an integer (instead of a float64
) you should call the UseNumber() method on your json.Decoder
instance before decoding. This will cause all JSON numbers to be decoded to the underlying type json.Number
instead of float64
.
The json.Number
type then provides an Int64()
method that you can call to get the number as an int64
, or the String()
method to get the number as a string
. For example:
js := `10`
var n any
dec := json.NewDecoder(strings.NewReader(js))
dec.UseNumber() // Call the UseNumber() method on the decoder before using it.
err := dec.Decode(&n)
if err != nil {
log.Fatal(err)
}
// Type assert the any value to a json.Number, and then call the Int64() method
// to get the number as a Go int64.
nInt64, err := n.(json.Number).Int64()
if err != nil {
log.Fatal(err)
}
// Likewise, you can use the String() method to get the number as a Go string.
nString := n.(json.Number).String()
fmt.Printf("type: %T; value: %v\n", n, n)
fmt.Printf("type: %T; value: %v\n", nInt64, nInt64)
fmt.Printf("type: %T; value: %v\n", nString, nString)
Struct tag directives
Using the struct tag json:"-"
on a struct field will cause it to be ignored when decoding JSON, even if the JSON input contains a corresponding key/value pair. The omitzero
and omitempty
struct tag directives do not have any effect on JSON decoding behavior.