Gopher says
Go doesn’t have classes - it has structs. Think of them as blueprints: you define the shape of your data, then attach methods to it. Combined with pointers, these three concepts are how every Go program is organized.
In baton-junction, every major component is a struct - Client holds the HTTP
connection, Connector ties everything together, and each builder
(userBuilder, groupBuilder, roleBuilder) wraps the client to sync one
resource type.
Structs: Data Blueprints
A struct groups related fields into a single type. Here’s the User struct from baton-junction/pkg/client/models.go:
type User struct {
ID string `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
Status string `json:"status"`
LastLogin *time.Time `json:"last_login,omitempty"`
} Every field has a name, a type, and an optional struct tag in backticks. Tags tell encoding/json how to map Go field names to JSON keys.
Exported vs Unexported
Go uses capitalization to decide what the rest of your program can see. This keeps internal implementation details hidden while exposing a clean public API. Compare two structs from the baton-junction connector:
// Exported fields - other packages can read u.Email
type User struct {
ID string `json:"id"`
Email string `json:"email"`
}
// Unexported fields - only the client package can access these
type Client struct {
httpClient *uhttp.BaseHttpClient
baseURL string
}
Uppercase = public. Lowercase = private. The compiler enforces this.
Methods & Receivers
Methods let you define behavior on your structs, similar to class methods in Python or other languages. Methods are functions attached to a type via a receiver:
func (c *Client) GetUsers(ctx context.Context, cursor string) ([]User, string, error) func (c *Client) GetUsers(ctx context.Context, cursor string) ([]User, string, error) {
var response ListResponse[User]
if err := c.doGet(ctx, "/api/users", cursor, &response); err != nil {
return nil, "", fmt.Errorf("baton-junction: failed to list users: %w", err)
}
return response.Data, response.NextCursor, nil
}
The (c *Client) receiver means this method belongs to *Client. Inside the body, c is like self in Python. This is a client-layer method, so it handles raw HTTP calls and returns errors. The builder layer (which you’ll build in Part 2) adds structured logging on top, extracting a logger from context and logging every outcome with zap fields.
Pointer Receivers
The * means “pointer receiver” - the method operates on the original struct, not a copy. Every method in baton-junction uses pointer receivers for efficiency and mutation safety.
The New() Constructor Pattern
Go has no constructors. Instead, a function named New creates and returns initialized structs:
func New(ctx, clientID, clientSecret, baseURL) (*Client, error) {
return &Client{field: value, ...}, nil
} func New(ctx context.Context, clientID, clientSecret, baseURL string) (*Client, error) {
// ... setup OAuth, create HTTP client ...
return &Client{
httpClient: baseClient,
baseURL: baseURL,
}, nil
}
Reading the Address-of Struct Syntax
This notation can look confusing at first. It’s actually two things combined: & (give me a pointer) and Type{...} (create a struct with these field values). Together, they create a struct and return its memory address in one step.
&Client{httpClient: baseClient, baseURL: url} Without the &, you get a value (a copy). With &, you get a pointer (a reference to the original). Constructor functions almost always return pointers so callers share the same instance.
Pointers in 30 Seconds
Pointers let you share data instead of copying it. When you pass a struct to a method, a pointer means the method can read and modify the original. That matters because every method in baton-junction uses pointer receivers - you need this to understand how they work.
| Syntax | Meaning | Example |
|---|---|---|
*T | Pointer to a T | *time.Time |
&v | Address of v | &Client{baseURL: url} |
*p | Value at pointer p | *u.LastLogin |
nil | No value | if u.LastLogin != nil |
Use *time.Time for optional fields - nil means “absent,” unlike time.Time which always has a zero value (year 0001).
JSON Tags
Go uses PascalCase for field names, but most APIs expect snake_case. Tags bridge that mismatch - they map Go naming (FirstName) to API naming (first_name):
Department string `json:"department,omitempty"` // omit if empty
LastLogin *time.Time `json:"last_login,omitempty"` // omit if nil
The omitempty option skips the field in JSON output when it holds a zero value.
baton-junction uses generics for API responses:
type ListResponse[T any] struct {
Data []T `json:"data"`
NextCursor string `json:"next_cursor"`
}[T any] means the struct works with any element type. Usage: ListResponse[User] gives Data the type []User.
Pointer Slices vs Value Slices
Why []*v2.Resource instead of []v2.Resource? When slices hold pointers ([]*Type), each element is a reference to the original struct rather than a copy of it. This is more memory-efficient for large structs and lets the caller modify elements directly. You’ll see []*v2.Resource, []*v2.Entitlement, and []*v2.Grant throughout the SDK.
Exercise: Build a Connector Struct
Define a Config struct with three exported string fields: BaseURL (json tag "base_url"), ClientID (json tag "client_id"), and Secret (json tag "secret,omitempty"). Then write a New function that takes those three strings as parameters and returns a *Config.
Gopher says
Structs, methods, and pointers are the backbone of every Go project. You’ll
see this pattern everywhere: define a struct, write a New() function, and
attach methods with pointer receivers.
In baton-junction, client.New() builds the API client, connector.New()
builds the connector, and every builder method uses a pointer receiver like
(o *userBuilder) to access the shared client.
Next: interfaces!
Next Lesson
Interfaces

