Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Pointers and Structs

Go has pointers, but no pointer arithmetic. Structs are the way to create complex data types.

Pointers

A pointer holds the memory address of a value. Think of it like a house address written on a slip of paper: the paper itself is not the house, but it tells you exactly where to find the house. When you pass a pointer to a function, you are handing over the address, not a copy of the entire house — the function can then go to that address and modify what is there. Key Syntax:
  • The type *T is a pointer to a T value. Its zero value is nil.
  • The & operator generates a pointer to its operand (“address of”).
  • The * operator denotes the pointer’s underlying value (“dereferencing”).
func main() {
    i, j := 42, 2701

    p := &i         // p is a pointer to i (p stores i's memory address)
    fmt.Println(*p) // read i through the pointer (prints 42)
    *p = 21         // set i through the pointer (i is now 21)
    fmt.Println(i)  // see the new value of i (prints 21)

    p = &j         // point to j instead
    *p = *p / 37   // divide j through the pointer
    fmt.Println(j) // see the new value of j (prints 73)
}
Why Use Pointers?
  1. Modify Function Arguments: Pass a pointer to allow a function to modify the original value.
  2. Avoid Copying Large Structs: Passing a pointer is more efficient than copying a large struct.
  3. Share Data: Multiple parts of your program can reference the same data.
No Pointer Arithmetic: Unlike C, Go has no pointer arithmetic. You cannot do p++ to move to the next memory address. This prevents common memory bugs.

Structs

A struct is a collection of fields.
type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}
    v.X = 4
    fmt.Println(v.X)
}

Pointers to Structs

Struct fields can be accessed through a struct pointer.
v := Vertex{1, 2}
p := &v
p.X = 1e9 // Equivalent to (*p).X
To access the field X of a struct when we have the struct pointer p we could write (*p).X. However, that notation is cumbersome, so the language permits us instead to write just p.X, without the explicit dereference.

Struct Literals

You can list just a subset of fields by using the Name: syntax. (And the order of named fields is irrelevant.)
var (
    v1 = Vertex{1, 2}  // has type Vertex
    v2 = Vertex{X: 1}  // Y:0 is implicit
    v3 = Vertex{}      // X:0 and Y:0
    p  = &Vertex{1, 2} // has type *Vertex
)

Methods

Go does not have classes. However, you can define methods on types. A method is a function with a special receiver argument.
type Vertex struct {
    X, Y float64
}

// Abs method has a receiver of type Vertex named v
func (v Vertex) Abs() float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
}

Pointer Receivers

You can declare methods with pointer receivers. This means the receiver type has the literal syntax *T for some type T. (Also, T cannot itself be a pointer such as *int.) Methods with pointer receivers can modify the value to which the receiver points (as Scale does here). Since methods often need to modify their receiver, pointer receivers are more common than value receivers.
func (v *Vertex) Scale(f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    v.Scale(10)
    fmt.Println(v.Abs()) // Output: 50
}

Choosing a Value or Pointer Receiver

There are two reasons to use a pointer receiver:
  1. Modify the receiver: The method needs to modify the value that the receiver points to.
  2. Performance: To avoid copying the value on each method call. This is more efficient if the struct is large.
Rule of Thumb: If in doubt, use a pointer receiver. In practice, most methods in production Go code use pointer receivers. The exceptions are small, immutable value types like time.Time, coordinate pairs, or color values.
Pitfall — Mixing Receiver Types: Be consistent with receiver types on a given struct. If some methods have pointer receivers and others have value receivers, it creates confusion about whether the type should be used as a value or a pointer, and it affects which interface implementations are satisfied:
type Vertex struct { X, Y float64 }

func (v Vertex) Abs() float64   { ... }  // Value receiver
func (v *Vertex) Scale(f float64) { ... } // Pointer receiver

// A *Vertex satisfies both methods, but a Vertex value
// only satisfies Abs() -- it cannot call Scale() through an interface.
// This inconsistency is a common source of "does not implement" errors.
The idiomatic rule: if any method needs a pointer receiver, make all methods use pointer receivers for that type.

Struct Tags

Struct tags are metadata strings attached to struct fields, commonly used for serialization, validation, and database mapping. They are one of Go’s most practical features for real-world development:
type User struct {
    ID        int       `json:"id" db:"id"`
    FirstName string    `json:"first_name" db:"first_name" validate:"required"`
    Email     string    `json:"email" db:"email" validate:"required,email"`
    Password  string    `json:"-" db:"password_hash"` // "-" means omit from JSON
    CreatedAt time.Time `json:"created_at" db:"created_at"`
}
The json:"-" tag is particularly important — it prevents sensitive fields like passwords from accidentally leaking in API responses. A senior engineer would say: “always tag your struct fields for JSON, and always use json:\"-\" for sensitive data.”

Interview Deep-Dive

Strong Answer:
  • Use a pointer receiver when: the method needs to mutate the receiver, the receiver is a large struct and you want to avoid copying it, or consistency demands it because other methods on the type already use pointer receivers. Use a value receiver when: the type is small and immutable (like time.Time, a coordinate pair, or a color value), and the method is a pure computation with no side effects.
  • The idiomatic rule is: if any method on a type needs a pointer receiver, make ALL methods use pointer receivers. Mixing creates confusion about whether the type should be used as a value or pointer, and it has concrete consequences for interface satisfaction.
  • Specifically, a *T can call both pointer-receiver and value-receiver methods (the compiler automatically takes the address of a value or dereferences a pointer as needed). But a plain T value can only call value-receiver methods through an interface. So if you have type Shape interface { Area() float64; Scale(f float64) }, and Scale uses a pointer receiver while Area uses a value receiver, then *Circle satisfies Shape but Circle does not. This mismatch produces the infamous “does not implement interface” error.
  • In production, I default to pointer receivers for almost everything. The exceptions are genuinely small value types (under 64 bytes), types that should be immutable by design, and types used as map keys (which must be comparable).
Follow-up: Go has no pointer arithmetic. What safety guarantees does this provide, and what can you still do that is unsafe?No pointer arithmetic means you cannot increment a pointer to walk through memory, calculate offsets manually, or cast a pointer to an arbitrary memory address. This eliminates buffer overflows, out-of-bounds pointer access, and use-after-free bugs that plague C codebases. However, Go does have the unsafe package which allows you to convert between pointer types, do pointer arithmetic via unsafe.Add, and bypass type safety entirely. The unsafe package is explicitly not covered by Go’s compatibility guarantee — code using it may break between Go versions. In production, you should almost never use unsafe directly. The legitimate uses are in low-level libraries doing memory-mapped I/O, FFI with C code, or performance-critical serialization. If you see unsafe in application code during a code review, that is a red flag.
Strong Answer:
  • Struct embedding is Go’s version of composition. When you embed a type inside a struct (by listing it without a field name), all of the embedded type’s methods and fields are “promoted” to the outer struct. You can call them directly on the outer struct without qualifying through the field name. But this is not inheritance — there is no “is-a” relationship. The outer struct contains the embedded type as a field, and the compiler syntactically rewrites outer.Method() to outer.EmbeddedField.Method().
  • When two embedded types have a method with the same name, Go does not pick one — it reports an ambiguity error at compile time if you try to call the method on the outer struct without qualifying which embedded type you mean. You must explicitly write outer.TypeA.Method() or outer.TypeB.Method(). You can also define a method directly on the outer struct with the same name, which shadows both embedded methods and resolves the ambiguity.
  • This is fundamentally different from inheritance. In Java, if class C extends B extends A, and B overrides a method from A, polymorphism means calling the method on a C instance calls B’s version. In Go embedding, there is no virtual dispatch — the promoted method still operates on the embedded type, not the outer type. The embedded type does not “know” about the outer type.
  • A practical example: embedding sync.Mutex in a struct gives the struct Lock() and Unlock() methods directly. But be careful with exported embedding — if you embed a sync.Mutex in an exported struct, the Lock/Unlock methods become part of your public API, which you probably do not intend. The convention is to embed unexported types or use a named field for types you do not want to expose.
Follow-up: A struct has a json:"email" tag on a field. What happens if you embed another struct that also has an email JSON tag?JSON marshaling follows the same promotion rules as method promotion. If the outer struct has a field with the same JSON tag as an embedded struct’s field, the outer field takes precedence (it shadows the embedded one). If two embedded structs both have a field with the same JSON tag and there is no shadowing field on the outer struct, json.Marshal silently omits the ambiguous field rather than picking one or erroring. This is a subtle source of bugs — a field can “disappear” from your JSON output because of an embedding conflict, and you get no compiler warning. The fix is to be explicit: either shadow the ambiguous field on the outer struct or use named fields instead of embedding.
Strong Answer:
  • Exported fields with zero values is the simplest approach: cfg := Config{Timeout: 5*time.Second}. The zero value of unset fields serves as the default. This works when zero values are meaningful defaults (0 for integers, "" for strings, false for booleans). The downside is that you cannot distinguish “user set this to zero” from “user did not set this” — if Port: 0 means “use default” but someone actually wants port 0, you have a problem. Validation is also deferred to runtime.
  • A constructor with a config struct is cleaner for required validation: NewServer(Config{Host: "0.0.0.0", Port: 8080}). You can validate required fields in the constructor and return an error. The struct can use pointer fields for optional values (Port *int) to distinguish “not set” from “set to zero.” Downside: the config struct can become unwieldy with many fields, and callers must construct the whole struct even to override one setting.
  • The functional options pattern (NewServer(WithHost("0.0.0.0"), WithPort(8080))) is the most flexible. Each option is a function that modifies the config. Options can validate their own values, compose freely, and can be stored in variables for reuse. The constructor sets defaults first, then applies options. This is the pattern used by grpc.NewServer, zap.New, and many other production Go libraries.
  • My recommendation: use exported fields for simple structs with fewer than 5 fields. Use functional options when you have many optional settings, need validation per option, or the API is a library consumed by other teams. The functional options pattern has become the de facto standard for configurable constructors in the Go ecosystem.
Follow-up: What is the json:"-" tag, and tell me about a production incident that could happen if you forget it?The json:"-" tag tells the JSON encoder to skip that field entirely. Without it, fields like Password, InternalToken, or SecretKey would be included in any JSON response that marshals the struct. I have seen a real incident where a user profile endpoint accidentally returned hashed passwords because the User struct was marshaled directly without a separate DTO, and the password field lacked the - tag. The passwords were bcrypt hashes, so they were not immediately exploitable, but it was still a security incident requiring user notification. The defense is defense in depth: always use json:"-" on sensitive fields, use separate response DTOs that only include fields meant for the client, and add integration tests that verify sensitive fields are absent from API responses.