How to define struct
You are building a service that tracks inventory. You have items with names, prices, and stock counts. In JavaScript, you might grab an object literal and start stuffing properties into it. In Python, a dictionary works fine for a quick script. Go refuses to let you be that loose. Go demands you define the shape of your data before you use it. That shape is a struct.
A struct is a blueprint for a value. It lists the fields and their types. Once you define the struct, you can create instances of it. The compiler checks every field access. If you try to access a field that doesn't exist, the build fails. This catches typos and logic errors early. Think of a struct like a fill-in-the-blank form at the DMV. The form defines exactly what information is required and where it goes. You can't write your license number in the address box. The form enforces the structure.
Minimal definition
Here's the simplest struct definition.
type User struct {
Name string // Name stores the user's display name
Age int // Age holds the user's age in years
}
The type keyword introduces a new type. User is the name. struct tells the compiler this is a composite type. The block contains the fields. Each field has a name and a type.
Notice the capital letters. In Go, capitalization controls visibility. Name starts with a capital letter, so any package can read or write it. If you wrote name with a lowercase letter, only the current package could touch it. There are no public or private keywords. The case of the first letter is the rule.
Structs define shape. The compiler enforces it.
Creating instances
Creating an instance uses a composite literal. You write the type name followed by a brace block. Inside, you list the fields and their values.
func main() {
u := User{
Name: "Alice", // Field names are required in composite literals
Age: 16,
}
fmt.Println(u.Name) // Access fields with dot notation
}
Field names are required in composite literals. This prevents bugs when you add or reorder fields later. If you omit a field, it gets the zero value for that type. Strings become empty strings. Integers become zero. Booleans become false.
u := User{Name: "Bob"}
fmt.Println(u.Age) // prints 0
Zero values are automatic. You don't need to initialize every field manually. The compiler fills in the blanks with safe defaults.
The braces and alignment matter for readability, but you don't need to worry about the exact spacing. gofmt handles that. Most editors run gofmt on save, so your code will always look consistent with the rest of the Go ecosystem. Trust the tool. Argue logic, not formatting.
Zero values save boilerplate. Trust the defaults.
Methods and receivers
Real code often needs methods attached to structs. Methods are functions that belong to a specific type.
type Server struct {
Addr string // Addr holds the host address
Port int // Port holds the listening port
}
// Start begins listening on the configured address.
func (s *Server) Start() {
// s is a pointer receiver, allowing the method to modify the struct
fmt.Printf("Listening on %s:%d\n", s.Addr, s.Port)
}
The receiver (s *Server) binds the method to the type. The receiver name s is a convention. It's short, usually one or two letters matching the type. You'll rarely see this or self in Go code. The receiver is a pointer here because the method might need to modify the struct or because the struct is large. If the struct is small and you only need to read fields, a value receiver works too.
Don't pass a *string. Strings are already cheap to pass by value. The header is small, and the underlying bytes are immutable. Pointers to strings add indirection without saving memory. Use pointers for structs that are large or need to be modified.
Composition over inheritance
Go has no inheritance. You can't subclass a struct. Instead, Go uses embedding. You can embed one struct inside another to reuse fields and methods.
type Employee struct {
User // Embeds User, promoting its fields and methods
Role string // Role is specific to Employee
}
The User field is anonymous. You can access Name directly on Employee. emp.Name works. This is called promotion. It's not inheritance. Employee is not a User. It just has a User inside it. The distinction matters when you have methods. If User has a Display method, Employee gets it too. But Employee can override it by defining its own Display method.
Composition is explicit. You see exactly what fields and methods come from where.
Tags and metadata
Structs often carry metadata called tags. Tags are string literals attached to fields. Libraries read tags to customize behavior.
type Config struct {
Host string `json:"host"` // json tag maps field to JSON key
Port int `json:"port,omitempty"` // omitempty skips zero values during marshaling
}
The json package reads these tags. omitempty tells the marshaler to skip the field if it has a zero value. Tags are just strings. The compiler doesn't enforce their format. It's up to the library to parse them correctly. You can define your own tags for your own libraries.
Tags are plumbing. Run them through every field that needs serialization or validation.
Comparison and equality
Structs are comparable if all their fields are comparable. You can use == to check equality.
u1 := User{Name: "Alice", Age: 16}
u2 := User{Name: "Alice", Age: 16}
fmt.Println(u1 == u2) // prints true
If a struct contains a slice, map, or function, the compiler rejects comparison with invalid operation: s1 == s2 (struct containing []string cannot be compared). You need to compare fields manually or use a library.
Typos in field names get caught immediately. If you type u.Namee, the compiler rejects the program with u.Namee undefined (type User has no field or method Namee). This saves hours of debugging compared to dynamic languages where a typo just returns undefined or None at runtime.
Decision matrix
Use a struct when you need a fixed set of fields with known types and want the compiler to enforce the shape. Use a map when the keys are dynamic or you need to look up values by an arbitrary key at runtime. Use an interface when you want to define behavior without committing to a specific implementation. Use a slice of structs when you need to store a collection of related items and want to preserve order.
Remember the mantra: accept interfaces, return structs. Your functions should take interfaces as arguments and return concrete structs as results.
Capitalization controls access. No keywords needed.