I had a recent situation where I needed to populate an existing struct with values, similar to the way
json.Unmarshal
works. These values would come from various sources, like URL query parameters, form data, path
variables, or something along those lines.
To give an example, I needed to map a URL like /newStudent?name=Albert&age=16&city=Zurich
onto a Golang struct like this:
type Student struct {
Name string
AgeInYears int `json:"age"`
City string
}
Initially, my thought was to use what we already have in the standard library: gather all the input values into a basic
map[string]string
, serialize this map into JSON using json.Marshal
, and then deserialize it back into the target
struct.
I soon realized this approach wasn't going to work. In the example above, all the input values are string
, but
AgeInYears
needs to be a different type. And the problems don't stop there: I also want to map path
parameters from an http.Request
, but there's no way to retrieve
all the existing values. You can only extract path values if you already know their names.
I needed a different solution. Essentially, I had to create my own version of json.Unmarshal
1. My goal was to
continue using the familiar json
struct tags, so fields could be renamed or skipped in a way that users already
expect. Additionally, the unmarshal function needed to work independently of the data source. The primary distinction of
our Unmarshal
function from json.Unmarshal
is that it is driven by the target struct, rather than the incoming data
(i.e., JSON).
To represent the source data, we'll introduce an interface SourceValue
. It will provide access to and conversion of
the actual values. To support nested mapping and to treat the root value the same as any other value, everything will
be considered a SourceValue
. Initially, we focus on handling int
and string
types, along with a mechanism to
retrieve child values. The interface also needs to account for scenarios where a child value doesn't exist for a given
name or when the SourceValue
can't be converted to the desired type, so the methods will return an error. Here's what
the interface might look like:
type SourceValue interface {
// Int returns the current value as an int64.
// Returns error ErrInvalidType if the value can not be represented as such.
Int() (int64, error)
// String returns the current value as a string.
// Returns error ErrInvalidType if the value can not be represented as such.
String() (string, error)
// Get returns a child value of this SourceValue if it exists.
// Returns error ErrInvalidType if the current SourceValue does not support
// child values. If the SourceValue does support children but is missing the
// requested child, ErrNoValue must be returned.
Get(key string) (SourceValue, error)
}
Given some SourceValue
that accesses URL query parameters,
we can now access the values from the previous example:
// get the name "Albert"
nameSource, _ := mySource.Get("name")
name, _ := nameSource.String()
// get his age
ageSource, _ := mySource.Get("age")
age, _ := ageSource.Int()
We are 80% done now, I guess. We just need to drive this by some function that
walks the Student
struct via reflection and performs the steps above for each
field. It should also do that recursively. This is left as an exercise to the … no, let's do it!
Starting small
In Go, we can use reflection to interact with types or values at runtime. We can determine the type of a
variable or access struct fields dynamically by leveraging the reflect
package, the reflect.Type
and the
reflect.Value
types.
Given a SourceValue
and a value of a primitive type like string
represented as target reflect.Value
, we can create a function to
extract the string
from the SourceValue
and assign it to the target
value using the Value.SetString
method.
func setString(source SourceValue, target reflect.Value) error {
stringValue, err := source.String()
if err != nil { return err }
target.SetString(stringValue)
return nil
}
With a bit of copy-and-paste, we can also create a setInt
method that functions in much the same way.
Both of those methods have the same type, let's call it setter
.
// A setter sets the target to the value
// extracted from the given SourceValue
type setter func(SourceValue, reflect.Value) error
When we encounter an unknown target value's type, we can determine the appropriate setter
by switching on the value's
reflect.Kind
. The Kind
represents the underlying type of a value. For instance, in type Name string
, Name
is a
defined type with string
as its underlying type.
Let's write a setterOf
method that returns either the setString
or the setInt
method depending on
an input type ty
.
func setterOf(ty reflect.Type) (setter, error) {
switch ty.Kind() {
case reflect.String:
return setString, nil
case reflect.Int, reflect.Int8, reflect.Int16, /* ... */:
return setInt, nil
default:
return nil, NotSupportedError{Type: ty}
}
}
With this we can now create a setter
for string
, pick the name
SourceValue
out of our student
and call the setter with a value holding the target string.
// get a string setter
nameSetter, _ := setterOf(reflect.TypeFor[string]())
// get the SourceValue for the name of our student
nameSource, _ := studentSource.Get("name")
var name string
var nameValue = reflect.ValueOf(name)
_ = nameSetter(nameSource, nameValue)
fmt.Println(name)
A wild panic appears!
panic: reflect: reflect.Value.SetString using unaddressable value
goroutine 18 [running]:
testing.tRunner.func1.2({0x13e640, 0x400011c2b0})
/go/src/testing/testing.go:1632 +0x1bc
testing.tRunner.func1()
/go/src/testing/testing.go:1635 +0x334
panic({0x13e640?, 0x400011c2b0?})
/go/src/runtime/panic.go:785 +0x124
reflect.flag.mustBeAssignableSlow(0x13e640?)
/go/src/reflect/value.go:257 +0x74
reflect.flag.mustBeAssignable(...)
/go/src/reflect/value.go:244
reflect.Value.SetString({0x13e640?, 0x2b5880?, 0x400011a210?}, {0x169cff, 0x6})
Nothing too bad, we just put the string directly into the value, but a reflect.Value
constructed like this is not
addressable, doh. Easy to fix, we just create the reflection value using a pointer to the name
variable and then
directly dereference it: reflect.ValueOf(&name).Elem()
. That way we end up with a reflect.Value
that is addressable
and can be updated. This also makes a lot more sense, as we actually want the setter to update name
somehow. We run
again and are greeted with the expected name. Perfect!
=== RUN TestSetter
Albert
--- PASS: TestSetter (0.00s)
PASS
Structs
I like to start at the bottom and work my way up: A simple sub problem of this is walk the Student
struct. Let's start
with that. Go recently introduced iterators and walking over all fields in a struct sounds a lot like a good use case for
that. We start with a function that gets a reflect.Type
holding the struct type we want to inspect and returns an
iterator over reflect.StructField
s.
func fieldsOf(ty reflect.Type) iter.Seq[reflect.StructField] {
return func(yield func(reflect.StructField) bool) {
// TODO
The reflect.Type
gives us access to the number of fields via the NumField() int
method. Looping over all field
indices and calling Field(index)
returns all the StructField
instances in the struct, including some for private fields.
Those can then be filtered out by checking IsExported()
. For now, we also filter out embedded fields - that's a
fun topic for later. This gives us the following loop:
for idx := range ty.NumField() {
fi := ty.Field(idx)
if !fi.IsExported() {
// skip not exported field
continue
}
if fi.Anonymous {
panic("not supported")
}
yield(fi)
}
We can now use the fieldsOf
function to iterate over all fields we want to serialize.
for fi := range fieldsOf(ty) {
// do some magic with reflect.StructType fi
}
To proceed, we need to identify the name we'll use for lookups in the SourceValue
interface for the
field in question. To achieve this, let's define a nameOf
function. This function takes the current fi
and returns
the name of the struct field, such as Name
or AgeInYears
.
func nameOf(fi reflect.StructField) string {
return fi.Name
}
This approach works, but we'd like it to also respect any existing json
struct tags. A json
struct tag can take the
form (name)?(,omitempty)?
2 or -
to indicate the field should be skipped. (Edgecase: a field can be named -
by
adding a comma behind the name -,
.)
To extract the name, we simply take the portion of the tag string before the first comma.
name := fi.Name
if tag := fi.Tag.Get("json"); tag != "" {
if tag == "-" {
// skip this field by returning an empty name
return ""
}
idx := strings.IndexByte(tag, ',')
switch {
case idx == -1:
// no comma, take the full tag as name
name = tag
case idx > 0:
// non empty name, take up to comma
name = tag[:idx]
}
}
return name
Now that this is settled, we can gather some information for each field.
We'll start by defining a field
type that combines Name
and Type
for later use. Additionally, we'll include an
Index
field to identify each field within a struct, making it easier to reference later. To bring it all together,
we'll implement a collectStructFields
method to handle the collection process.
func collectStructFields(ty reflect.Type) []field {
var fields []field
for fi := range fieldsOf(ty) {
name := nameOf(fi)
if name == "" {
continue
}
fields = append(fields, field{
Name: name, Type: fi.Type, Index: fi.Index,
})
}
return fields
}
Putting the pieces together
Let's move on to writing a function to populate each field of our Student
struct.
First, we'll start by listing the assignable fields in the struct using the previously defined collectStructFields
function. For each field, we'll retrieve its SourceValue
using the Get(string)
method. If no value is found, we
simply skip that field — just like json.Marshal
does. After adding some error handling, the function looks like this:
func setStudentStruct(source SourceValue, target reflect.Value) error {
ty := reflect.TypeFor[Student]()
fields := collectStructFields(ty)
for _, field := range fields {
fieldSource, err := source.Get(field.Name)
switch {
case errors.Is(err, ErrNoValue):
// no value in source, skip this field
continue
case err != nil:
return fmt.Errorf("lookup source field %q: %w", field.Name, err)
}
// TODO
}
return nil
}
Next, we use our setterOf
function to look up a setter
for the current field. The field's type is easily accessible
through field.Type
. To call the field's setter, we also need the fields reflect.Value
, which we can find using the
index stored in field.Index
. We'll pass that one as the target
parameter to the fields setter
.
With this information, we're ready to set the value for a struct field:
fieldSetter, err := setterOf(field.Type)
if err != nil {
return fmt.Errorf("lookup setter for %q: %w", field, err)
}
fieldValue := target.FieldByIndex(field.Index)
if err := fieldSetter(fieldSource, fieldValue); err != nil {
return fmt.Errorf("set field %q: %w", field.Name, err)
}
Finally, writing a quick test and running it confirms the expected result:
=== RUN TestSetStudentStruct
Student{Name:"Albert", AgeInYears:16, City:"Zurich"}`
--- PASS: TestSetStudentStruct (0.00s)
Streamlining
You might have noticed that the setStudentStruct
function isn't as generic (or reflective?) as it should be. It's
currently limited to working with Student
structs, which isn't ideal. On top of that, it only handles string
and
int
fields via setterOf
, so if we added a nested struct like an Address
field — containing city and zip code —
the current implementation will fail. Let's update our Student
struct to unmarshal while we're at this:
type Student struct {
Name string
Age int64
Address struct {
City string `json:"city"`
ZipCode int32 `json:"zip"`
}
}
We can enhance setStudentStruct
by allowing the struct type to be passed as a reflect.Type
parameter. This change enables the function to work with other structs, not just Student
.
The function could derive the struct type directly from the target
value. While this would be a reasonable solution, I
prefer the type to be passed in explicitly. This approach lets us separate the setter creation phase from the
setter execution phase, making it easier to test each part independently.
func setStruct(ty reflect.Type, source SourceValue, target reflect.Value) error {
fields := collectStructFields(ty)
// as before
}
With this change, the function doesn't meet the requirements for a setter
anymore. To address
that, we can splice it up a little.
By creating a function that constructs and returns a setter
for any struct type ty
,
we open the door for unmarshalling any struct type.
func makeSetStruct(ty reflect.Type) setter {
return func(source SourceValue, target reflect.Value) error {
fields := collectStructFields(ty)
// as before
}
}
With this setup, we can now improve the setterOf
function to support struct
types too. When it encounters a type of
kind Struct
, it can dynamically create a new setter for exactly this type using makeSetStruct
.
switch ty.Kind() {
case reflect.Struct:
return makeSetStruct(ty), nil
// ...
}
Make it nice
Now we can create a setter
capable of handling any struct type, including nested structs, as well as int
and
string
fields. To utilize the setter
, we'll need a target value to apply it to. As shown earlier, we can obtain one
for any pointer target
:
targetValue := reflect.ValueOf(target).Elem()
Let's wrap the reflection logic in a more user-friendly interface, much like how json.Unmarshal
operates:
func Unmarshal(source SourceValue, target any) error {
targetValue := reflect.ValueOf(target).Elem()
setter, err := setterOf(targetValue.Type())
if err != nil { return err }
return setter(source, targetValue)
}
Calling it now correctly fills all nested values:
var student Student
_ = Unmarshal(studentSource, &student)
Student is now filled correctly:
=== RUN TestSetStruct
Student{Name:Albert AgeInYears:16 Address:{City:Zurich ZipCode:8044}}
--- PASS: TestSetStruct (0.01s)
`
We now have the flexibility to implement different versions of the SourceValue
interface and unmarshal it to any
struct. For example, one implementation could fetch values from a url.Values
instance, while another might
extract path parameters from an http.Request
. We could even back it with a map[string]any
. For testing purposes, a
FakeSourceValue
could be created to return random strings and integers. The possibilities are limitless!
Some types know better
Skimming through the documentation of encoding/json
we can find a reference to encoding.TextUnmarshaler
.
The documentation of json.Unmarshal
tells us:
If the value implements
encoding.TextUnmarshaler
and the input is a JSON quoted string,Unmarshal
callsencoding.TextUnmarshaler.UnmarshalText
with the unquoted form of the string.
If encoding/json
supports this, there's no reason we shouldn't as well. A quick look at the standard library reveals
that this interface is implemented by types like net.IP
, and we might want to unmarshal IPs. Fortunately, adding
support for this is straightforward given our current setup.
All we need is a new setTextUnmarshaler
function that retrieves the value using String()
from the source and passes
it to the target's UnmarshalText()
method. Once implemented, we can update setterOf
to return this function as a
setter
whenever the target
type implements the TextUnmarshaler
interface.
var tyTextUnmarshaler = reflect.TypeFor[encoding.TextUnmarshaler]()
func setterOf(ty reflect.Type) (setter, error) {
if ty.Implements(tyTextUnmarshaler) {
return setTextUnmarshaler, nil
}
// ...
}
The implementation of setTextUnmarshaler
is straightforward.
func setTextUnmarshaler(source SourceValue, target reflect.Value) error {
text, err := source.String()
if err != nil {
return fmt.Errorf("get string value: %w", err)
}
m := target.Interface().(encoding.TextUnmarshaler)
return m.UnmarshalText([]byte(text))
}
Let's unmarshal this struct now. It contains the IP address of a host and optional port:
type Address struct {
Host net.IP
Port *int
}
=== RUN TestUnmarshalIP
de_test.go:160: setter for field "Host": type "net.IP" is not supported
--- FAIL: TestUnmarshalIP (0.00s)
The test failure makes sense because net.IP
itself doesn't implement TextUnmarshaler
; instead, *net.IP
does,
because UnmarshalText
is defined with a pointer receiver. To handle this, we need to update our setterOf
function to
check if the type implements TextUnmarshaler
on a pointer to the current type.
Here's how you we adjust the code:
if reflect.PointerTo(ty).Implements(tyTextUnmarshaler) {
return setTextUnmarshaler, nil
}
Additionally, when working with a pointer, we need to extract the interface from the pointer value using Addr()
:
m := target.Addr().Interface().(encoding.TextUnmarshaler)
Run the tests again:
=== RUN TestUnmarshalIP
de_test.go:160: setter for field "Port": type "*int" is not supported
--- FAIL: TestUnmarshalIP (0.00s)
Ah, crap — our code doesn't yet handle pointer values. Changing *int
back to int
allows the
Address
struct to unmarshal correctly, but that's not the behavior we're aiming for. Thankfully, adding support for
pointer values is straightforward. We can introduce a new setter
implementation and extend the setterOf
method to
handle cases where ty.Kind() == reflect.Pointer
.
There's an important distinction to keep in mind compared to the setTextUnmarshaler
function we just implemented. When
a target
parameter is passed to the setter
, it will be a pointer, but it won't point to anything yet. This means our
setter
needs to allocate a new instance of the target type (the pointee), and then apply the pointee's setter
to it.
To address this, we can follow a similar approach to makeSetStruct
and create a makeSetPointer
method. This method
will generate the appropriate setter
for the type:
func makeSetPointer(ty reflect.Type) (setter, error) {
pointeeType := ty.Elem()
pointeeSetter, err := setterOf(pointeeType)
if err != nil { return nil, err }
setter := func(source SourceValue, target reflect.Value) error {
// newValue is a pointer to a new instance of the pointeeType
newValue := reflect.New(pointeeType)
err := pointeeSetter(source, newValue.Elem())
if err != nil { return err }
// set target pointer to the new value
target.Set(newValue)
return nil
}
return setter, err
}
With this change, we finally achieve the desired result:
=== RUN TestUnmarshalIP
Host: {Host:127.0.0.1 Port:*8080}
--- PASS: TestUnmarshalIP (0.00s)
Caching
I've already mentioned the idea of separating the setter creation phase from the setter execution phase, and the
makeSetPointer
method is a great example of this in action. When makeSetPointer
is called, it prepares a setter
for the pointee type and returns a setter
for the pointer type. Once this returned setter
is invoked, it doesn't
need to inspect any types again — it can directly set the values as needed, even if called multiple times.
In contrast, our makeSetStruct
method doesn't yet follow this pattern. During setter execution, it loops over all
the struct fields, constructs a new setter
by calling setterOf(field.Type)
for each, and applies it to the target
value. If the struct setter
is invoked multiple times, this work is repeated unnecessarily.
To make this more efficient, we can move the construction of setters
for each struct field into the setter creation
phase of makeSetStruct
. The returned setter
can then close over this slice and simply invoke each setter
in turn
during execution.
func makeSetStruct(ty reflect.Type) (setter, error) {
fields := collectStructFields(ty)
// Added: prepare setters once during creation
var fieldSetters []setter
for _, field := range fields {
fieldSetter, err := setterOf(field.Type)
if err != nil { return nil, err }
fieldSetters = append(fieldSetters, fieldSetter)
}
setter := func(source SourceValue, target reflect.Value) error {
for idx, field := range fields {
// ... get fieldSource and fieldValue, same as before
// reference prepared setter during execution
fieldSetter := fieldSetters[idx]
// ... call fieldSetter, same as before
}
return nil
}
return setter, nil
}
This approach also improves testability, as it allows us to separate the process of creating a setter
for a struct
from the act of invoking it.
But that's actually not quite what I mean by caching. Consider a long-running process. Every time the Unmarshal
method is
called with a Student
reference, setterOf
is invoked to create a new setter
instance for the Student
type.
setter creation and setter execution, as described earlier.
Calling setterOf
with the Student
type will always produce the same (or equivalent) result. We can cache the
generated setter
based on its reflect.Type
, because setterOf
is pure (the function return values are identical for
identical arguments).
To make things thread-safe and avoid the complexity of locks, we'll skip using a sync.Mutex
and instead use a
sync.Map
. It's unfortunate that Go still does not have a generic sync.Map[K, V]
, but that's a rant for another time.
So, we'll begin by renaming our setterOf
method to makeSetterOf
, and then implement a new setterOf
method that:
- Checks the cache for an existing
setter
for the reflect typety
and returns it if found. - Delegates to
makeSetterOf
to create a newsetter
if no cached version exists. - Caches the new
setter
if no errors occur.
Easy:
var cachedSetters sync.Map
func setterOf(ty reflect.Type) (setter, error) {
cached, ok := cachedSetters.Load(ty)
if ok { return cached.(setter), nil }
setter, err := makeSetterOf(ty)
if err != nil { return nil, err }
cachedSetters.Store(ty, setter)
return setter, nil
}
One more issue
I was ready to call it quits here, but something kept nagging at me. Let's take a closer look at this struct:
type GitCommit struct {
Sha1 string
Parent *GitCommit
}
While this looks harmless, it breaks everything we have:
=== RUN TestUnmarshalGitCommit
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0x4020160370 stack=[0x4020160000, 0x4040160000]
fatal error: stack overflow
...
narf/zone.deser.setterOf({0x1bc6b8, 0x162900})
narf/zone.deser.makeSetPointer({0x1bc6b8?, 0x14a540?})
narf/zone.deser.makeSetterOf({0x1bc6b8, 0x14a540})
narf/zone.deser.setterOf({0x1bc6b8, 0x14a540})
narf/zone.deser.makeSetStruct({0x1bc6b8?, 0x162900?})
narf/zone.deser.makeSetterOf({0x1bc6b8, 0x162900})
narf/zone.deser.setterOf({0x1bc6b8, 0x162900})
narf/zone.deser.makeSetPointer({0x1bc6b8?, 0x14a540?})
narf/zone.deser.makeSetterOf({0x1bc6b8, 0x14a540})
narf/zone.deser.setterOf({0x1bc6b8, 0x14a540})
narf/zone.deser.makeSetStruct({0x1bc6b8?, 0x162900?})
narf/zone.deser.makeSetterOf({0x1bc6b8, 0x162900})
narf/zone.deser.setterOf({0x1bc6b8, 0x162900})
narf/zone.deser.makeSetPointer({0x1bc6b8?, 0x14a540?})
narf/zone.deser.makeSetterOf({0x1bc6b8, 0x14a540})
This is starting to get tricky. Our code runs into an infinite recursion. While building the setter
for GitCommit
,
we inspect the parent
field, which requires us to get a setter
for *GitCommit
. But to do that, we need to build a
setter
for GitCommit
first, which leads us right back to the same problem. Essentially, we're stuck in an endless
loop.
Why isn't this fixed by our setter
cache? The issue with the current approach is that we only cache the setter
for
GitCommit
once we've finished constructing it. Since we're constantly trying to construct it again while constructing
it, the cache doesn't help us break the cycle.
To handle this, we need a way to detect such cycles. A simple solution is to maintain a global variable that tracks the
types we're currently building setter
instances for. If we detect a cycle, we can return a placeholder setter
that
acts as a lazy proxy — it fetches the actual setter
implementation from the cache when invoked (during setter execution).
Think of it as a lazy setter. Here's how the flow would work, with stack depths indicated for clarity:
setterOf(1)
:
Mark GitCommit
as being constructed.
setterOf(1)
:
Call makeSetStruct(GitCommit)
.
makeSetStruct(2)
:
Encounter the parent
field and call setterOf(GitCommit)
.
setterOf(3)
:
Detect that GitCommit
is already being constructed.
Return a lazy setter
for GitCommit
.
makeSetStruct(2)
:
Return the struct setter
for GitCommit
.
setterOf(1)
:
Insert the completed setter
into the cache.
To prototype this, we can use a map
with a zero sized value type as a set
to track types under construction. We'll
probably want to exchange this with some thread safe type later. Lets do some minor adjustments to the setterOf
method:
type inConstructionTypes map[reflect.Type]struct{}
// global cache
var inConstruction = inConstructionTypes{}
func setterOf(ty reflect.Type) (setter, error) {
// ... do cache lookup as before
if _, ok := inConstruction[ty]; ok {
lazySetter := func(source SourceValue, target reflect.Value) error {
cached, _ := cachedSetters.Load(ty)
return cached.(setter)(source, target)
}
return lazySetter, nil
}
// mark in construction
inConstruction[ty] = struct{}{}
defer delete(inConstruction, ty)
// rest as before
}
As promising as this approach sounds, there's a significant issue to consider: concurrency. Imagine two goroutines calling
setterOf
at the same time. The first goroutine begins constructing a setter
for type A
. While it's still working,
the second goroutine also requests a setter
for A
. It sees that the setter
is under construction and gets a lazy
setter
as a placeholder.
Now, let's say the first goroutine realizes it cannot complete the setter
for A
because A
has a field of an
unsupported type. It returns an error and doesn't add the setter
to the cache.
Meanwhile, the second goroutine is left with a lazy setter
. When it eventually tries to use this placeholder, it will
panic because it attempts to fetch the actual setter
from the cache — only to find nothing there.
The root of the problem isn't really concurrency; it's the reliance on global state, specifically inConstruction
.
By using a shared global variable, we open the door to conflicts and unpredictable behavior.
A better solution is to make the setterOf
function reentrant.
Instead of depending on a global inConstruction
variable, we can pass this state explicitly through the call stack.
Each invocation of Unmarshal
would create its own tracking set, isolating the construction process. This
way, the issue disappears entirely.
// in Unmarshal
setter, err := setterOf(inConstructionTypes{}, targetValue.Type())
// in setterOf
setter, err := makeSetterOf(inConstruction, ty)
// similar changes in makeSetterOf, makeSetStruct, makeSetPointer
// ...
With everything in place, we can now unmarshal our git history!
=== RUN TestUnmarshalGitCommit
History: {"Sha1":"aaaa","Parent":{"Sha1":"bbbb","Parent":{"Sha1":"cccc","Parent":null}}}
--- PASS: TestUnmarshalGitCommit (0.00s)
Next steps
We've made significant progress, and I think this is a good place to wrap up. Here's a quick outlook on what remains:
More Types This one is straightforward. Extending support to types like
float64
andbool
should be trivial given the current setup.Slices Adding support for slices is another key improvement. This could be achieved by introducing a method
At(idx int) (SourceValue, error)
within theSourceValue
interface.Arrays Similar to slices. Here is an interesting quote from the
encoding/json
documentation:To unmarshal a JSON array into a Go array, Unmarshal decodes JSON array elements into corresponding Go array elements. If the Go array is smaller than the JSON array, the additional JSON array elements are discarded. If the JSON array is smaller than the Go array, the additional Go array elements are set to zero values.
Who needs errors, am I right?
There's plenty of potential for further refinement, but for now, we've built a solid foundation.
Thank you for making it this far — now go build something!
Discuss this article on reddit.
With blackjack and hookers, dah.
Technically there is also ,string
, but we disregard this for now.