In the previous tutorial, you learned about slices and maps. Now it is time to learn how to create your own types with structs.
Go does not have classes. Instead, it uses structs and methods. Structs hold data, and methods add behavior. This is simpler than class-based languages like Java or Python.
Defining a Struct
A struct groups related fields together:
package main
import "fmt"
type User struct {
Name string
Email string
Age int
}
func main() {
// Create a struct with field names
user1 := User{
Name: "Alex",
Email: "alex@example.com",
Age: 25,
}
fmt.Println(user1)
// Access fields with dot notation
fmt.Println("Name:", user1.Name)
fmt.Println("Email:", user1.Email)
// Update a field
user1.Age = 26
fmt.Println("New age:", user1.Age)
}
Output:
{Alex alex@example.com 25}
Name: Alex
Email: alex@example.com
New age: 26
Zero Value Structs
If you don’t set a field, it gets its zero value:
package main
import "fmt"
type Config struct {
Host string
Port int
Debug bool
Timeout float64
}
func main() {
// All fields get zero values
var cfg Config
fmt.Printf("Host: %q\n", cfg.Host) // ""
fmt.Printf("Port: %d\n", cfg.Port) // 0
fmt.Printf("Debug: %t\n", cfg.Debug) // false
fmt.Printf("Timeout: %.1f\n", cfg.Timeout) // 0.0
}
This means every struct is always in a valid state. No null or undefined fields like in some other languages.
Anonymous Structs
You can create a struct without defining a type. This is useful for one-time use:
package main
import "fmt"
func main() {
point := struct {
X, Y int
}{
X: 10,
Y: 20,
}
fmt.Println("Point:", point)
}
Methods
A method is a function with a receiver. The receiver connects the method to a type:
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
// Method with a value receiver
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// Method with a value receiver
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Printf("Width: %.1f, Height: %.1f\n", rect.Width, rect.Height)
fmt.Printf("Area: %.1f\n", rect.Area())
fmt.Printf("Perimeter: %.1f\n", rect.Perimeter())
}
Output:
Width: 10.0, Height: 5.0
Area: 50.0
Perimeter: 30.0
The (r Rectangle) before the method name is the receiver. It is like self in Python or this in Java, but you choose the name. Go convention is to use a short name (one or two letters) based on the type name.
Value Receivers vs Pointer Receivers
A value receiver gets a copy of the struct. A pointer receiver gets a reference to the original:
package main
import "fmt"
type Counter struct {
Value int
}
// Value receiver — works on a COPY (cannot modify the original)
func (c Counter) Current() int {
return c.Value
}
// Pointer receiver — works on the ORIGINAL (can modify it)
func (c *Counter) Increment() {
c.Value++
}
// Pointer receiver — can modify the original
func (c *Counter) Reset() {
c.Value = 0
}
func main() {
counter := Counter{Value: 0}
fmt.Println("Start:", counter.Current()) // 0
counter.Increment()
counter.Increment()
counter.Increment()
fmt.Println("After 3 increments:", counter.Current()) // 3
counter.Reset()
fmt.Println("After reset:", counter.Current()) // 0
}
Output:
Start: 0
After 3 increments: 3
After reset: 0
When to use which?
- Use a pointer receiver when the method needs to modify the struct
- Use a pointer receiver when the struct is large (avoids copying)
- Use a value receiver when the method only reads data and the struct is small
- Be consistent: if one method uses a pointer receiver, all methods on that type should use pointer receivers
Constructor Functions
Go does not have constructors like Java or Python. Instead, you write a function that returns a new instance. By convention, the function name starts with New:
package main
import "fmt"
type User struct {
Name string
Email string
Age int
}
// Constructor function — returns a pointer to a new User
func NewUser(name, email string, age int) *User {
return &User{
Name: name,
Email: email,
Age: age,
}
}
func (u *User) String() string {
return fmt.Sprintf("%s (%s, age %d)", u.Name, u.Email, u.Age)
}
func main() {
user := NewUser("Alex", "alex@example.com", 25)
fmt.Println(user.String())
}
Output:
Alex (alex@example.com, age 25)
Returning a pointer (*User) is common in Go. It avoids copying the struct when the function returns.
Composition with Embedding
Go does not have inheritance. Instead, it uses composition. You embed one struct inside another:
package main
import "fmt"
type Address struct {
Street string
City string
Zip string
}
func (a Address) FullAddress() string {
return fmt.Sprintf("%s, %s %s", a.Street, a.City, a.Zip)
}
type Employee struct {
Name string
Salary float64
Address // Embedded struct — no field name
}
func main() {
emp := Employee{
Name: "Alex",
Salary: 75000,
Address: Address{
Street: "123 Main St",
City: "Berlin",
Zip: "10115",
},
}
// Access Address fields directly — they are "promoted"
fmt.Println("Name:", emp.Name)
fmt.Println("City:", emp.City) // Promoted field
fmt.Println("Street:", emp.Street) // Promoted field
// Access the embedded struct's method directly
fmt.Println("Address:", emp.FullAddress()) // Promoted method
}
Output:
Name: Alex
City: Berlin
Street: 123 Main St
Address: 123 Main St, Berlin 10115
When you embed a struct, all its fields and methods are “promoted.” You can access them as if they belong to the outer struct. This is composition, not inheritance. The Employee is not an Address. It has an Address.
Multiple Embeddings
You can embed multiple structs:
package main
import (
"fmt"
"time"
)
type Timestamps struct {
CreatedAt time.Time
UpdatedAt time.Time
}
type SoftDelete struct {
Deleted bool
DeletedAt time.Time
}
type Article struct {
Title string
Content string
Timestamps
SoftDelete
}
func NewArticle(title, content string) *Article {
now := time.Now()
return &Article{
Title: title,
Content: content,
Timestamps: Timestamps{
CreatedAt: now,
UpdatedAt: now,
},
}
}
func main() {
article := NewArticle("Go Structs", "Learn about structs in Go")
fmt.Println("Title:", article.Title)
fmt.Println("Created:", article.CreatedAt.Format("2006-01-02"))
fmt.Println("Deleted:", article.Deleted) // false (zero value)
}
This pattern is common in Go. You create small, reusable structs (Timestamps, SoftDelete) and embed them where needed.
Struct Tags
Struct tags are metadata you attach to fields. They don’t change the struct’s behavior, but other packages can read them:
package main
import (
"encoding/json"
"fmt"
)
type Product struct {
Name string `json:"name"`
Price float64 `json:"price"`
InStock bool `json:"in_stock"`
Internal string `json:"-"` // "-" means skip this field
}
func main() {
product := Product{
Name: "Keyboard",
Price: 49.99,
InStock: true,
Internal: "secret data",
}
// Convert struct to JSON
data, err := json.Marshal(product)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(string(data))
}
Output:
{"name":"Keyboard","price":49.99,"in_stock":true}
The json:"name" tag tells the JSON encoder to use name instead of Name. The json:"-" tag tells it to skip the field entirely. You will use struct tags a lot when building web APIs.
A Complete Example
Here is a program that uses structs, methods, and composition together:
package main
import "fmt"
// Base types
type Dimensions struct {
Width float64
Height float64
}
func (d Dimensions) Area() float64 {
return d.Width * d.Height
}
// Shape types with embedding
type Box struct {
Label string
Dimensions
Depth float64
}
func (b Box) Volume() float64 {
return b.Area() * b.Depth // Area() is promoted from Dimensions
}
type Shelf struct {
Name string
Boxes []Box
}
func NewShelf(name string) *Shelf {
return &Shelf{
Name: name,
Boxes: make([]Box, 0),
}
}
func (s *Shelf) AddBox(box Box) {
s.Boxes = append(s.Boxes, box)
}
func (s *Shelf) TotalVolume() float64 {
total := 0.0
for _, box := range s.Boxes {
total += box.Volume()
}
return total
}
func (s *Shelf) Summary() {
fmt.Printf("Shelf: %s (%d boxes)\n", s.Name, len(s.Boxes))
for _, box := range s.Boxes {
fmt.Printf(" - %s: %.1f x %.1f x %.1f = %.1f cubic units\n",
box.Label, box.Width, box.Height, box.Depth, box.Volume())
}
fmt.Printf(" Total volume: %.1f cubic units\n", s.TotalVolume())
}
func main() {
fmt.Println("=== GO-7: Structs, Methods, and Composition ===")
fmt.Println()
shelf := NewShelf("Storage A")
shelf.AddBox(Box{
Label: "Books",
Dimensions: Dimensions{Width: 30, Height: 25},
Depth: 20,
})
shelf.AddBox(Box{
Label: "Electronics",
Dimensions: Dimensions{Width: 40, Height: 30},
Depth: 25,
})
shelf.AddBox(Box{
Label: "Tools",
Dimensions: Dimensions{Width: 20, Height: 15},
Depth: 10,
})
shelf.Summary()
}
Output:
=== GO-7: Structs, Methods, and Composition ===
Shelf: Storage A (3 boxes)
- Books: 30.0 x 25.0 x 20.0 = 15000.0 cubic units
- Electronics: 40.0 x 30.0 x 25.0 = 30000.0 cubic units
- Tools: 20.0 x 15.0 x 10.0 = 3000.0 cubic units
Total volume: 48000.0 cubic units
Common Mistakes
1. Forgetting to use a pointer receiver when modifying a struct.
// Wrong — this modifies a copy, not the original
func (c Counter) Increment() {
c.Value++ // This change is lost
}
// Correct — use a pointer receiver
func (c *Counter) Increment() {
c.Value++ // This modifies the original
}
If your method needs to change the struct, you must use a pointer receiver (*Counter).
2. Comparing structs with incomparable fields.
type Data struct {
Values []int // Slices are NOT comparable
}
a := Data{Values: []int{1, 2}}
b := Data{Values: []int{1, 2}}
// a == b // Compile error: struct containing []int cannot be compared
Structs are comparable only if all their fields are comparable. Slices, maps, and functions are not comparable.
3. Field name conflicts with embedding.
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 = "from A" // This works — be explicit
If two embedded structs have fields with the same name, you must access them explicitly.
Source Code
You can find the complete source code for this tutorial on GitHub:
Related Articles
- Go Tutorial #6: Arrays, Slices, and Maps — Slices and maps
- Go Tutorial #4: Functions and Error Handling — Functions and the error pattern
What’s Next?
In the next tutorial, Go Tutorial #8: Interfaces and Polymorphism, you will learn:
- How interfaces work in Go — implicit implementation
- The empty interface and
any - Type assertions and type switches
- Common standard library interfaces like
io.Readerandfmt.Stringer - The “accept interfaces, return structs” principle
This is part 7 of the Go Tutorial series. Follow along to learn Go from scratch.