Skip to content
ioob.dev
Go back

Go Basics Part 6 — Structs and Methods

· 3 min read
Go Series (6/12)
  1. Go Basics Part 1 — Variables, Constants, and Types
  2. Go Basics Part 2 — Conditionals and Loops
  3. Go Basics Part 3 — Functions
  4. Go Basics Part 4 — Error Handling
  5. Go Basics Part 5 — Arrays, Slices, and Maps
  6. Go Basics Part 6 — Structs and Methods
  7. Go Part 7 — Interfaces
  8. Go Part 8 — Pointers
  9. Go Part 9 — Goroutines and Channels
  10. Go Part 10 — Concurrency Patterns
  11. Go Part 11 — Packages and Modules
  12. Go Part 12 — Generics and Practical Patterns
Table of contents

Table of contents

struct Declaration

Go has no classes. Instead, it bundles data into structs and attaches methods to them, implementing object orientation in its own way.

package main

import "fmt"

type User struct {
    Name  string
    Email string
    Age   int
}

func main() {
    // Named fields
    u1 := User{
        Name:  "Alice",
        Email: "alice@example.com",
        Age:   30,
    }

    // Positional (not recommended)
    u2 := User{"Bob", "bob@example.com", 25}

    fmt.Println(u1)       // {Alice alice@example.com 30}
    fmt.Println(u2.Name)  // Bob
}

Using named fields is recommended. If fields are added later, positional initialization will break.

Uninitialized fields get the zero value. This is consistent with Go’s philosophy discussed in Part 1.

package main

import "fmt"

type Config struct {
    Host    string
    Port    int
    Debug   bool
}

func main() {
    c := Config{Host: "localhost"}
    fmt.Println(c)  // {localhost 0 false}
    // Port is 0, Debug is false
}

Methods — Attaching a Receiver to a Function

In Go, a method is a function attached to a specific type. You declare a receiver before the function name.

package main

import "fmt"

type Rectangle struct {
    Width  float64
    Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

func main() {
    rect := Rectangle{Width: 10, Height: 5}
    fmt.Println("area:", rect.Area())           // area: 50
    fmt.Println("perimeter:", rect.Perimeter())  // perimeter: 30
}

In func (r Rectangle), r is the receiver. It plays a similar role to Java’s this, but you name it yourself. The convention is to use the first letter of the type name in lowercase: r for Rectangle, u for User, and so on.

Pointer Receiver vs Value Receiver

In the example above, (r Rectangle) is a value receiver. The struct is copied when the method is called. If you want to modify the original, you need a pointer receiver.

package main

import "fmt"

type Counter struct {
    Value int
}

// Value receiver — cannot modify the original
func (c Counter) Display() {
    fmt.Println("current:", c.Value)
}

// Pointer receiver — modifies the original
func (c *Counter) Increment() {
    c.Value++
}

func main() {
    counter := Counter{Value: 0}
    counter.Increment()
    counter.Increment()
    counter.Display()  // current: 2
}

Note that Increment’s receiver is *Counter. c.Value++ directly modifies the Value of the original struct. With a value receiver, only the copy would change and the original would remain untouched.

The criteria for choosing are clear:

Mixing pointer and value receivers for the same type is not recommended. The convention is to be consistent, and in most cases, everything uses pointer receivers. It’s a way of preparing in advance for the struct potentially growing larger.

Embedding — Reuse Without Inheritance

Go has no inheritance. Instead, it composes types through embedding.

package main

import "fmt"

type Address struct {
    City    string
    ZipCode string
}

func (a Address) FullAddress() string {
    return a.City + " " + a.ZipCode
}

type Employee struct {
    Name string
    Address  // Embedding — just the type, no field name
}

func main() {
    emp := Employee{
        Name: "Alice",
        Address: Address{
            City:    "Seoul",
            ZipCode: "06234",
        },
    }

    // Direct access to embedded fields
    fmt.Println(emp.City)           // Seoul
    fmt.Println(emp.FullAddress())  // Seoul 06234

    // This also works
    fmt.Println(emp.Address.City)   // Seoul
}

When you embed Address without a name in Employee, the fields and methods of Address become accessible as if they belong to Employee. This is closer to delegation than inheritance.

Comparing inheritance and embedding with a diagram makes the distinction easy to understand.

flowchart LR
    subgraph Inherit[Inheritance - is-a]
        P1[Person] -->|inherits| E1[Employee]
    end
    subgraph Embed[Embedding - has-a]
        E2[Employee] --> A[Address field]
        A -.method promotion.- E2
    end
    Note1[Employee IS a Person]
    Note2[Employee HAS an Address]
    Inherit --- Note1
    Embed --- Note2

This distinction matters. With inheritance, “Employee is an Address” would hold, but with embedding, “Employee has an Address” is correct. Go favors “has-a” over “is-a” relationships, taking the position that this is more flexible for code reuse.

You can embed multiple types simultaneously.

type Logger struct{}
func (l Logger) Log(msg string) { fmt.Println("[LOG]", msg) }

type Validator struct{}
func (v Validator) Validate(s string) bool { return len(s) > 0 }

type Service struct {
    Logger
    Validator
}

Service can use both the Log and Validate methods. This pattern of composing new types by combining needed functionality is a core Go programming style.

Constructor Pattern — NewXxx

Go has no constructor syntax. Instead, the convention is to create a factory function starting with New.

package main

import (
    "errors"
    "fmt"
)

type Server struct {
    host string
    port int
}

func NewServer(host string, port int) (*Server, error) {
    if host == "" {
        return nil, errors.New("host cannot be empty")
    }
    if port < 1 || port > 65535 {
        return nil, errors.New("port must be in the range 1-65535")
    }
    return &Server{host: host, port: port}, nil
}

func (s *Server) Address() string {
    return fmt.Sprintf("%s:%d", s.host, s.port)
}

func main() {
    srv, err := NewServer("localhost", 8080)
    if err != nil {
        fmt.Println("creation failed:", err)
        return
    }
    fmt.Println(srv.Address())  // localhost:8080
}

Note that NewServer returns a pointer *Server. It allows validation logic and can return errors. This is why it’s more flexible than Java constructors.

Fields starting with lowercase (host, port) are not directly accessible from outside the package. In Go, uppercase initial letters mean exported (public), lowercase means unexported (private). Since values can only be set through the constructor function, encapsulation is naturally achieved.

Struct Tags

You can attach metadata to struct fields. These are called tags.

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age,omitempty"`
}

func main() {
    u := User{
        Name:  "Alice",
        Email: "alice@example.com",
    }

    data, _ := json.Marshal(u)
    fmt.Println(string(data))
    // {"name":"Alice","email":"alice@example.com"}
    // Age is 0, so omitempty excludes it
}

Tags are enclosed in backticks (`) and follow the key:"value" format. encoding/json, encoding/xml, ORM libraries, and others read these tags to determine behavior.

Here are some commonly used tag patterns.

type Product struct {
    ID    int    `json:"id" db:"product_id"`
    Name  string `json:"name" validate:"required"`
    Price int    `json:"price" validate:"min=0"`
    Note  string `json:"-"`  // Excluded from JSON serialization
}

json:"-" completely excludes the field from serialization. Use it when you don’t want to expose internal fields externally. The db tag maps to database column names, and the validate tag is used by validation libraries.

Tags are read via reflection and operate at runtime. Typos won’t cause compile errors, so it’s a good habit to check with tools like go vet.


This concludes the Go Basics series. From variables and types through to structs and methods, we’ve made a complete tour of Go’s fundamental syntax. With this foundation, you should have the basic skills to read and write simple Go programs.

Go is a language where idiomatic patterns matter more than syntax volume. Things like if err != nil, naming conventions, and interface usage are absorbed naturally by reading lots of code. The official Effective Go documentation is a great starting point.


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Go Basics Part 5 — Arrays, Slices, and Maps
Next Post
Go Part 7 — Interfaces