Structs and Methods
Go uses structs instead of classes. Combined with methods and interfaces, they provide a flexible approach to organizing code without traditional OOP inheritance.
Defining Structs
type Person struct {
Name string
Age int
Email string
}
type Point struct {
X, Y float64 // Same type can share a line
}
// Empty struct (zero bytes, useful for signals and sets)
type signal struct{}
Creating Struct Instances
// Zero value (all fields are zero values)
var p Person
// p.Name = "", p.Age = 0, p.Email = ""
// Literal with field names (preferred)
p := Person{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
}
// Literal without field names (fragile, avoid)
p := Person{"Alice", 30, "alice@example.com"}
// Partial initialization
p := Person{Name: "Bob"} // Age=0, Email=""
// Pointer to struct
p := &Person{Name: "Charlie", Age: 25}
// Using new (returns pointer with zero values)
p := new(Person) // *Person
Accessing Fields
p := Person{Name: "Alice", Age: 30}
// Direct access
fmt.Println(p.Name) // Alice
p.Age = 31 // Modify
// Through pointer (Go auto-dereferences)
ptr := &p
fmt.Println(ptr.Name) // Alice (not (*ptr).Name)
ptr.Age = 32 // Works directly
Anonymous Structs
Useful for one-off data structures:
// Inline definition and initialization
config := struct {
Host string
Port int
}{
Host: "localhost",
Port: 8080,
}
// Common in tests
tests := []struct {
input string
expected int
}{
{"hello", 5},
{"world", 5},
{"", 0},
}
Embedded Structs (Composition)
Go favors composition over inheritance:
type Address struct {
Street string
City string
Country string
}
type Person struct {
Name string
Age int
Address // Embedded (anonymous field)
}
// Usage
p := Person{
Name: "Alice",
Age: 30,
Address: Address{
Street: "123 Main St",
City: "Boston",
Country: "USA",
},
}
// Fields are "promoted" - can access directly
fmt.Println(p.City) // Boston
fmt.Println(p.Address.City) // Also works
Embedding and Name Conflicts
type A struct {
Name string
}
type B struct {
Name string
}
type C struct {
A
B
}
c := C{}
// c.Name // Ambiguous! Compile error
c.A.Name // OK
c.B.Name // OK
Methods
Methods are functions with a receiver argument:
type Rectangle struct {
Width, Height float64
}
// Method with value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Method with value receiver
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
// Usage
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50
fmt.Println(rect.Perimeter()) // 30
Value vs Pointer Receivers
type Counter struct {
value int
}
// Value receiver (works on copy)
func (c Counter) Value() int {
return c.value
}
// Pointer receiver (modifies original)
func (c *Counter) Increment() {
c.value++
}
func (c *Counter) Add(n int) {
c.value += n
}
// Usage
counter := Counter{}
counter.Increment() // Go auto-converts to (&counter).Increment()
fmt.Println(counter.Value()) // 1
When to Use Pointer Receivers
Use pointer receivers when:
- The method modifies the receiver
- The struct is large (avoid copying)
- Consistency: if one method needs a pointer, use it for all
type Config struct {
// Many fields...
DatabaseURL string
CacheSize int
// ...more fields
}
// Even if not modifying, use pointer for large structs
func (c *Config) Validate() error {
if c.DatabaseURL == "" {
return errors.New("database URL required")
}
return nil
}
Methods on Non-Struct Types
You can define methods on any type you own:
type MyInt int
func (m MyInt) Double() MyInt {
return m * 2
}
func (m MyInt) IsPositive() bool {
return m > 0
}
x := MyInt(5)
fmt.Println(x.Double()) // 10
fmt.Println(x.IsPositive()) // true
Constructors
Go doesn't have constructors. Use factory functions:
type Server struct {
host string
port int
}
// Constructor function (convention: New<Type> or New)
func NewServer(host string, port int) *Server {
return &Server{
host: host,
port: port,
}
}
// With validation
func NewServer(host string, port int) (*Server, error) {
if port < 0 || port > 65535 {
return nil, errors.New("invalid port")
}
return &Server{host: host, port: port}, nil
}
// Usage
server := NewServer("localhost", 8080)
Functional Options Pattern
For complex constructors with many optional parameters:
type Server struct {
host string
port int
timeout time.Duration
maxConn int
}
type ServerOption func(*Server)
func WithPort(port int) ServerOption {
return func(s *Server) {
s.port = port
}
}
func WithTimeout(timeout time.Duration) ServerOption {
return func(s *Server) {
s.timeout = timeout
}
}
func WithMaxConnections(max int) ServerOption {
return func(s *Server) {
s.maxConn = max
}
}
func NewServer(host string, opts ...ServerOption) *Server {
s := &Server{
host: host,
port: 8080, // default
timeout: 30 * time.Second, // default
maxConn: 100, // default
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer("localhost",
WithPort(9000),
WithTimeout(60*time.Second),
)
Struct Tags
Metadata for struct fields, used by encoding packages:
type User struct {
ID int `json:"id" db:"user_id"`
Name string `json:"name" db:"user_name"`
Email string `json:"email,omitempty"` // Omit if empty
Password string `json:"-"` // Never encode
CreatedAt time.Time `json:"created_at"`
}
// Usage with encoding/json
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
data, _ := json.Marshal(user)
// {"id":1,"name":"Alice","email":"alice@example.com","created_at":"..."}
Reading Tags with Reflection
import "reflect"
type User struct {
Name string `validate:"required,min=2"`
}
t := reflect.TypeOf(User{})
field, _ := t.FieldByName("Name")
tag := field.Tag.Get("validate")
fmt.Println(tag) // "required,min=2"
Comparing Structs
Structs are comparable if all their fields are comparable:
type Point struct {
X, Y int
}
p1 := Point{1, 2}
p2 := Point{1, 2}
p3 := Point{3, 4}
fmt.Println(p1 == p2) // true
fmt.Println(p1 == p3) // false
// Structs with slices/maps are NOT comparable
type Data struct {
Values []int // Slice
}
// d1 == d2 // Compile error!
Struct Copying
Structs are copied by value:
p1 := Person{Name: "Alice", Age: 30}
p2 := p1 // Copy
p2.Name = "Bob"
fmt.Println(p1.Name) // Still "Alice"
// Careful with pointers inside structs!
type Container struct {
Data *[]int
}
c1 := Container{Data: &[]int{1, 2, 3}}
c2 := c1 // Shallow copy!
(*c2.Data)[0] = 100
fmt.Println((*c1.Data)[0]) // 100 - modified!
// Deep copy manually
c2 := Container{Data: &[]int{}}
*c2.Data = append(*c2.Data, *c1.Data...)
Common Patterns
Builder Pattern
type QueryBuilder struct {
table string
columns []string
where string
limit int
}
func (qb *QueryBuilder) Select(cols ...string) *QueryBuilder {
qb.columns = cols
return qb
}
func (qb *QueryBuilder) From(table string) *QueryBuilder {
qb.table = table
return qb
}
func (qb *QueryBuilder) Where(condition string) *QueryBuilder {
qb.where = condition
return qb
}
func (qb *QueryBuilder) Limit(n int) *QueryBuilder {
qb.limit = n
return qb
}
func (qb *QueryBuilder) Build() string {
// Build SQL string...
}
// Usage
query := new(QueryBuilder).
Select("id", "name").
From("users").
Where("age > 18").
Limit(10).
Build()
Self-Referential Structs
type Node struct {
Value int
Next *Node // Pointer to same type
}
// Linked list
head := &Node{Value: 1}
head.Next = &Node{Value: 2}
head.Next.Next = &Node{Value: 3}
// Tree
type TreeNode struct {
Value int
Left *TreeNode
Right *TreeNode
}
Next Steps
Continue to 07-interfaces.md to learn about interfaces and polymorphism.