Gopher says
Time to organize passengers! Go has two main collection types: slices (ordered lists that grow dynamically) and maps (key-value lookups like dictionaries). baton-junction uses both in nearly every function.
In baton-junction, every List method builds a []*v2.Resource slice with
append, and we use token maps to track pagination cursor state across
pages.
Slices
A slice is a dynamic-length, ordered sequence. Declare one with var, then grow it with append:
var resources []*v2.Resource
for _, u := range users {
r, err := userResource(&u)
if err != nil {
return nil, "", nil, err
}
resources = append(resources, r)
}
This is the declare-append pattern - it appears in every List method in baton-junction.
Key Slice Operations
The most important thing to remember about append is that it returns a new slice. Here’s what the syntax looks like:
resources = append(resources, r) append returns a new slice - you must reassign the result. This is the #1 beginner mistake.
Understanding nil Across Types
nil means “no value” in Go, but it behaves differently depending on the type. This is one of the trickier parts of Go. A nil slice is safe to use, but a nil map will crash if you try to write to it:
var s []string
var m map[string]int
var p *User Nil Slices Are Safe
One nice thing about Go slices: you don’t need to initialize them before using them. A nil slice (one that hasn’t been assigned yet) works just fine with len, append, and for range:
var s []string // nil - s == nil is true
len(s) // 0
append(s, "hello") // works - allocates and returns a one-element slice
for _, v := range s { // zero iterations
}
Slice Literals
Sometimes you already know exactly what goes in the slice at the time you write it. Instead of starting empty and appending, you can declare all the elements inline:
[]Type{
value1,
value2,
value3,
} return []connectorbuilder.ResourceSyncerV2{
newUserBuilder(m.client),
newGroupBuilder(m.client),
newRoleBuilder(m.client),
}
Iterating with for range
Most of the time you won’t know what’s in a slice ahead of time. You’ll get data from an API, a database, or some other source and need to loop through it. Go’s for range is how you walk through every element in a slice (or a map):
for _, u := range users {
...
} for _, u := range users {
r, err := userResource(&u)
resources = append(resources, r)
}
Each iteration yields an index and an element. The _ discards the index (Go requires you to use every declared variable).
Maps
Slices are great when you care about order, but sometimes you need to look something up by a key instead of by position. That’s what maps are for. A map is an unordered collection of key-value pairs, like a dictionary in Python or an object in JavaScript.
map[KeyType]ValueType{
key1: value1,
key2: value2,
} Here are three ways to create them:
// Literal - known contents
profile := map[string]interface{}{
"first_name": u.FirstName,
"last_name": u.LastName,
}
// make - populate later
attrs := make(map[string]string)
// Package-level lookup table
var attrsLookup = map[string]string{
"first_name": "first_name",
"email": "email",
}
Writing to a nil map panics. Always initialize with make or a literal before writing.
Maps with any Values
In the map literal above, map[string]interface{} (or its alias map[string]any) means “a map where keys are strings and values can be anything.” You’ll see this when building JSON-like structures with mixed value types.
Dereferencing Pointers
You may notice *u.LastLogin in the conditional-append section below. The * in front of a pointer variable means “get the actual value at this pointer.” If u.LastLogin is a *time.Time (pointer to a Time), then *u.LastLogin gives you the time.Time value itself. Always check for nil first to avoid a panic:
if u.LastLogin != nil {
opts = append(opts, resource.WithLastLogin(*u.LastLogin))
}
The Comma-Ok Pattern
When you read from a map, you might get a zero value back. But does that mean the key exists with a zero value, or that it doesn’t exist at all? Go solves this with the “comma-ok” pattern, which gives you a second boolean return value: true if the key exists, false if it doesn’t:
value, ok := myMap["key"]
if ok {
// key exists, value is the real value
}
// if ok is false, value is the zero value for that type
This same pattern works with type assertions (v, ok := x.(T)) and channel receives. The ok boolean eliminates ambiguity between “key exists with zero value” and “key doesn’t exist.”
Iterating Maps
Just like slices, you can loop through maps with for range. Each iteration gives you a key and a value:
for c1Name, apiName := range attrsLookup {
val, found := actions.GetStringArg(args, c1Name)
if found && val != "" {
attrs[apiName] = val
}
}
Map iteration order is not guaranteed - Go deliberately randomizes it.
Start with known elements, then add more based on conditions:
opts := []resource.UserTraitOption{
resource.WithEmail(u.Email, true),
resource.WithUserLogin(u.Username),
}
if u.CreatedAt.Unix() > 0 {
opts = append(opts, resource.WithCreatedAt(u.CreatedAt))
}
if u.LastLogin != nil {
opts = append(opts, resource.WithLastLogin(*u.LastLogin))
}The final slice has 2 to 4 elements depending on the user’s data.
Quick Check
What happens if you write to a nil map in Go? (one word)
Exercise: Transform and Lookup
Implement two functions. activeEmails should return a slice of email addresses for users whose status is "active". emailLookup should return a map from email address to user pointer.
Gopher says
Slices and maps are Go’s workhorses. The declare-append loop and comma-ok pattern will become second nature.
In baton-junction, you’ll see var resources []*v2.Resource followed by an
append loop in every builder’s List method - it’s the most common collection
pattern in the codebase.
Next stop: packages and visibility!
Next Lesson
Packages & Visibility

