By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: Clean Code in Go (Part 2): Structs, Methods, and Composition Over Inheritance | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > Clean Code in Go (Part 2): Structs, Methods, and Composition Over Inheritance | HackerNoon
Computing

Clean Code in Go (Part 2): Structs, Methods, and Composition Over Inheritance | HackerNoon

News Room
Last updated: 2025/11/09 at 7:03 PM
News Room Published 9 November 2025
Share
Clean Code in Go (Part 2): Structs, Methods, and Composition Over Inheritance | HackerNoon
SHARE

This is the second article in my clean code series. You can read the first part here.

https://hackernoon.com/clean-code-functions-and-error-handling-in-go-from-chaos-to-clarity-part-1?embedable=true

Introduction: Why OOP in Go Isn’t What You Think

I’ve seen hundreds of developers try to write Go like Java, creating inheritance hierarchies that don’t exist and fighting the language every step of the way. “Go has no classes!” — the first shock for developers with Java/C# background. The second — “How to live without inheritance?!”. Relax, Go offers something better: composition through embedding, interfaces without explicit implementation, and clear rules for methods.

Common struct/method mistakes I’ve observed:

  • Using value receivers with mutexes: ~25% cause data races
  • Mixing receiver types: ~35% of struct methods
  • Creating getters/setters for everything: ~60% of structs
  • Trying to implement inheritance: ~40% of new Go developers

After 6 years of working with Go, I can say: the difference between fighting the language and flowing with it usually comes down to understanding structs and methods properly.

Receivers: The Go Developer’s Main Dilemma

Value vs Pointer Receiver

This is question #1 in interviews and code reviews. Here’s a simple rule that covers 90% of cases:

// Value receiver - for immutable methods
func (u User) FullName() string {
    return fmt.Sprintf("%s %s", u.FirstName, u.LastName)
}

// Pointer receiver - when changing state
func (u *User) SetEmail(email string) error {
    if !isValidEmail(email) {
        return ErrInvalidEmail
    }
    u.Email = email
    u.UpdatedAt = time.Now()
    return nil
}

Rules for Choosing a Receiver

type Account struct {
    ID      string
    Balance decimal.Decimal
    mutex   sync.RWMutex
}

// Rule 1: If even one method requires a pointer receiver,
// ALL methods should use pointer receiver (consistency)

// BAD: mixed receivers
func (a Account) GetBalance() decimal.Decimal { // value receiver
    return a.Balance
}

func (a *Account) Deposit(amount decimal.Decimal) { // pointer receiver
    a.Balance = a.Balance.Add(amount)
}

// GOOD: consistent receivers
func (a *Account) GetBalance() decimal.Decimal {
    a.mutex.RLock()
    defer a.mutex.RUnlock()
    return a.Balance
}

func (a *Account) Deposit(amount decimal.Decimal) error {
    if amount.LessThanOrEqual(decimal.Zero) {
        return ErrInvalidAmount
    }

    a.mutex.Lock()
    defer a.mutex.Unlock()
    a.Balance = a.Balance.Add(amount)
    return nil
}

When to Use Pointer Receiver

  1. Method modifies state
  2. Struct contains mutex (otherwise it will be copied!)
  3. Large struct (avoid copying)
  4. Consistency (if at least one method requires pointer)
// Struct with mutex ALWAYS pointer receiver
type Cache struct {
    data map[string]interface{}
    mu   sync.RWMutex
}

// DANGEROUS: value receiver copies mutex!
func (c Cache) Get(key string) interface{} { // BUG!
    c.mu.RLock() // Locking a COPY of mutex
    defer c.mu.RUnlock()
    return c.data[key]
}

// CORRECT: pointer receiver
func (c *Cache) Get(key string) interface{} {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.data[key]
}

Constructors and Factory Functions

Go doesn’t have constructors in the classical sense, but there’s the New* idiom:

// BAD: direct struct creation
func main() {
    user := &User{
        ID:    generateID(), // What if we forget?
        Email: "[email protected]",
        // CreatedAt not set!
    }
}

// GOOD: factory function guarantees initialization
func NewUser(email string) (*User, error) {
    if !isValidEmail(email) {
        return nil, ErrInvalidEmail
    }

    return &User{
        ID:        generateID(),
        Email:     email,
        CreatedAt: time.Now(),
        UpdatedAt: time.Now(),
    }, nil
}

Functional Options Pattern

For structs with many optional parameters:

type Server struct {
    host     string
    port     int
    timeout  time.Duration
    maxConns int
    tls      *tls.Config
}

// Option - function that modifies Server
type Option func(*Server)

// Factory functions for options
func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithTLS(config *tls.Config) Option {
    return func(s *Server) {
        s.tls = config
    }
}

func WithMaxConnections(max int) Option {
    return func(s *Server) {
        s.maxConns = max
    }
}

// Constructor accepts required parameters and options
func NewServer(host string, port int, opts ...Option) *Server {
    server := &Server{
        host:     host,
        port:     port,
        timeout:  30 * time.Second, // defaults
        maxConns: 100,
    }

    // Apply options
    for _, opt := range opts {
        opt(server)
    }

    return server
}

// Usage - reads like prose
server := NewServer("localhost", 8080,
    WithTimeout(60*time.Second),
    WithMaxConnections(1000),
    WithTLS(tlsConfig),
)

Encapsulation Through Naming

Go has no private/public keywords. Instead — the case of the first letter:

type User struct {
    ID        string    // Public field (Exported)
    Email     string    
    password  string    // Private field (Unexported)
    createdAt time.Time // Private
}

// Public method
func (u *User) SetPassword(pwd string) error {
    if len(pwd) < 8 {
        return ErrWeakPassword
    }

    hashed, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
    if err != nil {
        return fmt.Errorf("hash password: %w", err)
    }

    u.password = string(hashed)
    return nil
}

// Private helper
func (u *User) validatePassword(pwd string) error {
    return bcrypt.CompareHashAndPassword([]byte(u.password), []byte(pwd))
}

// Public method uses private one
func (u *User) Authenticate(pwd string) error {
    if err := u.validatePassword(pwd); err != nil {
        return ErrInvalidCredentials
    }
    return nil
}

Composition Through Embedding

Instead of inheritance, Go offers embedding. This is NOT inheritance, it’s composition:

// Base struct
type Person struct {
    FirstName string
    LastName  string
    BirthDate time.Time
}

func (p Person) FullName() string {
    return fmt.Sprintf("%s %s", p.FirstName, p.LastName)
}

func (p Person) Age() int {
    return int(time.Since(p.BirthDate).Hours() / 24 / 365)
}

// Employee embeds Person
type Employee struct {
    Person     // Embedding - NOT inheritance!
    EmployeeID string
    Department string
    Salary     decimal.Decimal
}

// Employee can override Person's methods
func (e Employee) FullName() string {
    return fmt.Sprintf("%s (%s)", e.Person.FullName(), e.EmployeeID)
}

// Usage
emp := Employee{
    Person: Person{
        FirstName: "John",
        LastName:  "Doe",
        BirthDate: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC),
    },
    EmployeeID: "EMP001",
    Department: "Engineering",
}

fmt.Println(emp.FullName())       // John Doe (EMP001) - overridden method
fmt.Println(emp.Age())            // 34 - method from Person
fmt.Println(emp.FirstName)        // John - field from Person

Embedding Interfaces

type Reader interface {
    Read([]byte) (int, error)
}

type Writer interface {
    Write([]byte) (int, error)
}

// ReadWriter embeds both interfaces
type ReadWriter interface {
    Reader
    Writer
}

// Struct can embed interfaces for delegation
type LoggedWriter struct {
    Writer // Embed interface
    logger *log.Logger
}

func (w LoggedWriter) Write(p []byte) (n int, err error) {
    n, err = w.Writer.Write(p) // Delegate to embedded Writer
    w.logger.Printf("Wrote %d bytes, err: %v", n, err)
    return n, err
}

// Usage
var buf bytes.Buffer
logged := LoggedWriter{
    Writer: &buf,
    logger: log.New(os.Stdout, "WRITE: ", log.LstdFlags),
}

logged.Write([]byte("Hello, World!"))

Method Chaining (Builder Pattern)

type QueryBuilder struct {
    table   string
    columns []string
    where   []string
    orderBy string
    limit   int
}

// Each method returns *QueryBuilder for chaining
func NewQuery(table string) *QueryBuilder {
    return &QueryBuilder{
        table:   table,
        columns: []string{"*"},
    }
}

func (q *QueryBuilder) Select(columns ...string) *QueryBuilder {
    q.columns = columns
    return q
}

func (q *QueryBuilder) Where(condition string) *QueryBuilder {
    q.where = append(q.where, condition)
    return q
}

func (q *QueryBuilder) OrderBy(column string) *QueryBuilder {
    q.orderBy = column
    return q
}

func (q *QueryBuilder) Limit(n int) *QueryBuilder {
    q.limit = n
    return q
}

func (q *QueryBuilder) Build() string {
    query := fmt.Sprintf("SELECT %s FROM %s", 
        strings.Join(q.columns, ", "), q.table)

    if len(q.where) > 0 {
        query += " WHERE " + strings.Join(q.where, " AND ")
    }

    if q.orderBy != "" {
        query += " ORDER BY " + q.orderBy
    }

    if q.limit > 0 {
        query += fmt.Sprintf(" LIMIT %d", q.limit)
    }

    return query
}

// Usage - reads like SQL
query := NewQuery("users").
    Select("id", "name", "email").
    Where("active = true").
    Where("created_at > '2024-01-01'").
    OrderBy("created_at DESC").
    Limit(10).
    Build()

// SELECT id, name, email FROM users WHERE active = true AND created_at > '2024-01-01' ORDER BY created_at DESC LIMIT 10

Thread-Safe Structs

// BAD: race condition
type Counter struct {
    value int
}

func (c *Counter) Inc() {
    c.value++ // Race when accessed concurrently!
}

// GOOD: protected with mutex
type SafeCounter struct {
    mu    sync.Mutex
    value int
}

func (c *SafeCounter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

// EVEN BETTER: using atomic
type AtomicCounter struct {
    value atomic.Int64
}

func (c *AtomicCounter) Inc() {
    c.value.Add(1)
}

func (c *AtomicCounter) Value() int64 {
    return c.value.Load()
}

Anti-patterns and How to Avoid Them

1. Getters/Setters for All Fields

// BAD: Java-style getters/setters
type User struct {
    name string
    age  int
}

func (u *User) GetName() string { return u.name }
func (u *User) SetName(name string) { u.name = name }
func (u *User) GetAge() int { return u.age }
func (u *User) SetAge(age int) { u.age = age }

// GOOD: export fields or use methods with logic
type User struct {
    Name string
    age  int // private because validation needed
}

func (u *User) SetAge(age int) error {
    if age < 0 || age > 150 {
        return ErrInvalidAge
    }
    u.age = age
    return nil
}

func (u *User) Age() int {
    return u.age
}

2. Huge Structs

// BAD: God Object
type Application struct {
    Config     Config
    Database   *sql.DB
    Cache      *redis.Client
    HTTPServer *http.Server
    GRPCServer *grpc.Server
    Logger     *log.Logger
    Metrics    *prometheus.Registry
    // ... 20 more fields
}

// GOOD: separation of concerns
type App struct {
    config   *Config
    services *Services
    servers  *Servers
}

type Services struct {
    DB    Database
    Cache Cache
    Auth  Authenticator
}

type Servers struct {
    HTTP *HTTPServer
    GRPC *GRPCServer
}

Practical Tips

  1. Always use constructors for structs with invariants
  2. Be consistent with receivers within a type
  3. Prefer composition over inheritance (which doesn’t exist)
  4. Embedding is not inheritance, it’s delegation
  5. Protect concurrent access with a mutex or channels
  6. Don’t create getters/setters without necessity

Struct and Method Checklist

  • Constructor New* for complex initialization
  • Consistent receivers (all pointer or all value)
  • Pointer receiver for structs with a mutex
  • Private fields for encapsulation
  • Embedding instead of inheritance
  • Thread-safety when needed
  • Minimal getters/setters

Conclusion

Structs and methods in Go are an exercise in simplicity. No classes? Great, less complexity. No inheritance? Perfect, the composition is clearer. The key is not to drag patterns from other languages but to use Go idioms.

In the next article, we’ll dive into interfaces — the real magic of Go. We’ll discuss why small interfaces are better than large ones, what interface satisfaction means, and why “Accept interfaces, return structs” is the golden rule.

How do you handle the transition from OOP languages to Go’s composition model? What patterns helped you the most? Share your experience in the comments!

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article Is It Possible For A Plane To Outrun Earth’s Rotation? – BGR Is It Possible For A Plane To Outrun Earth’s Rotation? – BGR
Next Article Get a MacBook Air for the lowest-ever price while supplies last Get a MacBook Air for the lowest-ever price while supplies last
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

Today&apos;s NYT Connections: Sports Edition Hints, Answers for Nov. 10 #413
Today's NYT Connections: Sports Edition Hints, Answers for Nov. 10 #413
News
Microsoft Will No Longer Let You View Photos in the Windows 11 Phone Link App
Microsoft Will No Longer Let You View Photos in the Windows 11 Phone Link App
News
Patches Proposed For Radeon GCN 1.1 GPUs To Use AMDGPU Linux Driver By Default
Patches Proposed For Radeon GCN 1.1 GPUs To Use AMDGPU Linux Driver By Default
Computing
AI factories face a long payback period but trillions in upside –  News
AI factories face a long payback period but trillions in upside – News
News

You Might also Like

Patches Proposed For Radeon GCN 1.1 GPUs To Use AMDGPU Linux Driver By Default
Computing

Patches Proposed For Radeon GCN 1.1 GPUs To Use AMDGPU Linux Driver By Default

5 Min Read
Taking Zeekr private, Geely emphasizes willingness to cooperate with global capital markets · TechNode
Computing

Taking Zeekr private, Geely emphasizes willingness to cooperate with global capital markets · TechNode

3 Min Read
Taobao and Tmall deepen partnership with Xiaohongshu to power seamless cross-platform shopping · TechNode
Computing

Taobao and Tmall deepen partnership with Xiaohongshu to power seamless cross-platform shopping · TechNode

1 Min Read
Ant Group deploys domestic GPU cluster with tens of thousands of units · TechNode
Computing

Ant Group deploys domestic GPU cluster with tens of thousands of units · TechNode

1 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?