Level 3 /

Structs, Methods & Pointers

Time to build the first real piece of the train: the passenger car. In the rail yard workshop, Gopher assembles structural components: panels, wheels, window frames. A car is not just a box on wheels. It has specific parts, and each part has a job.

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:

keywordtype name (exported)composite typefield namefield typeJSON struct tag
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:

keywordreceiver (like self)method nameparametersreturn types
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:

keywordconstructor nameparametersreturns pointer + errorstruct literal with &
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.

address-of operatorstruct typefield assignments
&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.

SyntaxMeaningExample
*TPointer to a T*time.Time
&vAddress of v&Client{baseURL: url}
*pValue at pointer p*u.LastLogin
nilNo valueif 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.

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!

Explore the related connector code files

Next Lesson

Interfaces

Continue