Structs are how Go organizes data. No classes, no inheritance hierarchy, just fields grouped together. When AI generates Go code for an APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses., a database model, or a configuration object, it produces a struct. Your job is to evaluate whether the fields, tags, and pointer/value decisions make sense.
Defining and creating structs
type User struct {
ID int
Username string
Email string
IsActive bool
}
// Named field initialization - always use this
alice := User{
ID: 1,
Username: "alice",
Email: "alice@example.com",
IsActive: true,
}
// Positional - fragile, breaks when fields are reordered
bob := User{2, "bob", "bob@example.com", true}| Initialization style | Readability | Breaks on reorder? | Use it? |
|---|---|---|---|
Named fields User{Name: "x"} | Clear | No | Yes, always |
Positional User{"x", 1} | Opaque | Yes | No, except tiny structs |
Zero value User{} or var u User | Obvious | No | When defaults are fine |
Pointer &User{Name: "x"} | Clear | No | When you need a pointer |
Structs are value types
This is the single most important thing to understand. When you assign a struct to another variable or pass it to a function, Go copies every field.
type Point struct {
X, Y int
}
p1 := Point{X: 10, Y: 20}
p2 := p1 // copy
p2.X = 999
fmt.Println(p1) // {10 20} - unchangedIn functions:
func resetPoint(p Point) {
p.X = 0
p.Y = 0
// This modifies a copy - caller's Point is unchanged
}
func resetPointPtr(p *Point) {
p.X = 0
p.Y = 0
// This modifies the original through the pointer
}*struct) parameter and modify its fields.Exported vs unexported fields
Go uses capitalization for visibility:
type Config struct {
Host string // exported - visible outside the package
Port int // exported
apiKey string // unexported - only visible within this package
timeout int // unexported
}| First letter | Visibility | JSON default behavior |
|---|---|---|
Uppercase (Host) | Exported, accessible from other packages | Marshaled |
Lowercase (host) | Unexported, package-private | Ignored by encoding/json |
encoding/json, they won't marshal or unmarshal. If the AI-generated struct produces empty JSON or silently drops fields, check the capitalization.Struct tags
Tags are string metadata on struct fields that libraries read at runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary. via reflection. The most common use is JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. mapping.
type APIResponse struct {
UserID int `json:"user_id"`
FullName string `json:"full_name"`
Email string `json:"email,omitempty"`
Password string `json:"-"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}JSON tag options
| Tag | Effect | When to use |
|---|---|---|
json:"name" | Maps Go field to JSON key name | Almost always, Go uses PascalCase, JSON uses snake_case |
json:"-" | Field is completely excluded from JSON | Passwords, internal IDs, secrets |
json:"name,omitempty" | Excluded if zero value | Optional fields in API responses |
json:",string" | Encodes number as JSON string | When API consumers expect string numbers |
user := APIResponse{
UserID: 1,
FullName: "Alice",
Password: "secret123",
// Email is zero value, IsActive is false
}
data, _ := json.Marshal(user)
// {"user_id":1,"full_name":"Alice","created_at":"0001-01-01T00:00:00Z"}
// Password excluded (-), Email excluded (omitempty + zero value)josn instead of json, mismatched backticks, or missing commas. Go won't warn you. The tag is silently ignored, and your JSON output uses Go's default field names (UserID instead of user_id). Always verify the marshaled output matches what you expect.Struct embedding (composition)
Go doesn't have inheritance. Instead, you embed one struct inside another. The embedded struct's fields and methods are "promoted", accessible directly on the outer struct.
type Timestamps struct {
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type User struct {
Timestamps // embedded - fields promoted
ID int `json:"id"`
Username string `json:"username"`
}
u := User{
Timestamps: Timestamps{
CreatedAt: time.Now(),
},
ID: 1,
Username: "alice",
}
// Access promoted fields directly
fmt.Println(u.CreatedAt)
// Also accessible via the embedded type
fmt.Println(u.Timestamps.CreatedAt)Embedding vs named fields
| Approach | Syntax | Field access | Use case |
|---|---|---|---|
| Embedding | Timestamps (no field name) | u.CreatedAt (promoted) | Shared field groups, method promotion |
| Named field | Stamps Timestamps | u.Stamps.CreatedAt | When you want explicit namespacing |
Embedding conflict: shadowing
When two embedded structs have fields with the same name, neither is promoted, you must access them explicitly:
type A struct {
Name string
}
type B struct {
Name string
}
type C struct {
A
B
}
c := C{}
// c.Name // compile error: ambiguous selector
c.A.Name = "a" // explicit - works
c.B.Name = "b" // explicit - worksConstructor pattern
Go doesn't have constructors. The convention is a New function that returns a pointer:
type Server struct {
host string
port int
routes map[string]Handler
logger *log.Logger
}
func NewServer(host string, port int) *Server {
return &Server{
host: host,
port: port,
routes: make(map[string]Handler), // initialize the map!
logger: log.Default(),
}
}New function is critical because it initializes internal state (maps, channels, mutexes) that would panic if left at zero value. When AI generates a struct with map or channel fields but no constructor, that's a bug waiting to happen.Comparing structs
Structs are comparable (with ==) if all their fields are comparable:
type Point struct {
X, Y int
}
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true
// Structs with slices or maps are NOT comparable
type Data struct {
Values []int
}
// d1 == d2 // compile errorIf your struct has slice or map fields, you need reflect.DeepEqual() or a custom comparison function.