Table of contents
- struct Declaration
- Methods — Attaching a Receiver to a Function
- Pointer Receiver vs Value Receiver
- Embedding — Reuse Without Inheritance
- Constructor Pattern — NewXxx
- Struct Tags
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:
- Pointer receiver: When you need to modify the struct, or when the struct is large (copying cost is a concern)
- Value receiver: When you only read the struct, or when the struct is small
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.




Loading comments...