Go offers composition through embedding, interfaces without explicit implementation, and clear rules for methods. The difference between fighting the language and flowing with it usually comes down to understanding structs and methods properly.Go offers composition through embedding, interfaces without explicit implementation, and clear rules for methods. The difference between fighting the language and flowing with it usually comes down to understanding structs and methods properly.

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

2025/11/10 04:06

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: "test@test.com", // 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!

Disclaimer: The articles reposted on this site are sourced from public platforms and are provided for informational purposes only. They do not necessarily reflect the views of MEXC. All rights remain with the original authors. If you believe any content infringes on third-party rights, please contact service@support.mexc.com for removal. MEXC makes no guarantees regarding the accuracy, completeness, or timeliness of the content and is not responsible for any actions taken based on the information provided. The content does not constitute financial, legal, or other professional advice, nor should it be considered a recommendation or endorsement by MEXC.

You May Also Like

Stunning CME Bitcoin Futures Gap: $960 Weekend Volatility Creates Massive Trading Opportunity

Stunning CME Bitcoin Futures Gap: $960 Weekend Volatility Creates Massive Trading Opportunity

BitcoinWorld Stunning CME Bitcoin Futures Gap: $960 Weekend Volatility Creates Massive Trading Opportunity Have you ever wondered why CME Bitcoin futures sometimes open with massive price gaps? This week, traders witnessed a stunning $960 gap as the market reopened, creating both excitement and opportunity for savvy investors. Understanding these CME Bitcoin futures movements can unlock valuable trading insights. What Exactly Are CME Bitcoin Futures Gaps? CME Bitcoin futures gaps occur when there’s a significant difference between Friday’s closing price and Monday’s opening price. The Chicago Mercantile Exchange (CME) closes for the weekend, while Bitcoin’s spot market operates 24/7. This creates a fascinating dynamic where weekend volatility directly impacts Monday’s CME Bitcoin futures opening. This recent $960 gap saw contracts jump from $104,160 to $105,120. The substantial movement demonstrates how weekend trading activity in the spot market can create immediate opportunities when CME Bitcoin futures resume trading. Why Do CME Futures Gaps Matter for Traders? Traders closely monitor CME Bitcoin futures gaps because they often present predictable trading patterns. Many investors watch for these gaps to fill, meaning the price tends to move back toward the original closing level. However, this isn’t guaranteed – sometimes gaps expand further. Price discovery: Gaps reveal hidden weekend market sentiment Trading signals: Large gaps often indicate strong momentum Risk management: Understanding gaps helps set appropriate stop losses Opportunity identification: Gaps can highlight undervalued or overvalued conditions How Can You Profit from CME Bitcoin Futures Gaps? Successful traders develop strategies around CME Bitcoin futures gaps. The key lies in understanding whether a gap will fill or continue expanding. Historical data shows that most CME gaps eventually fill, but timing is crucial. Consider these approaches when trading CME Bitcoin futures gaps: Wait for confirmation before entering trades Use multiple time frame analysis Monitor spot market correlations Set realistic profit targets based on gap size What Makes This $960 Gap So Significant? This particular CME Bitcoin futures gap of $960 represents substantial weekend volatility. The size indicates strong market movement during the CME’s closure, potentially driven by major news events or institutional activity. Such large gaps in CME Bitcoin futures often precede significant price trends. Traders should note that larger gaps typically take longer to fill, if they fill at all. The current CME Bitcoin futures environment suggests continued institutional interest and market maturity. Mastering CME Bitcoin Futures Trading Understanding CME Bitcoin futures gaps provides a competitive edge in cryptocurrency trading. These patterns offer valuable insights into market sentiment and potential price movements. By monitoring CME Bitcoin futures activity, traders can make more informed decisions and potentially capitalize on these predictable patterns. The $960 gap serves as a powerful reminder that cryptocurrency markets never sleep, even when traditional exchanges close. This creates unique opportunities for alert traders who understand how CME Bitcoin futures interact with continuous spot markets. Frequently Asked Questions What causes CME Bitcoin futures gaps? CME Bitcoin futures gaps occur because the exchange closes on weekends while Bitcoin’s spot market trades continuously. Weekend price movements create the difference between Friday’s close and Monday’s open. How often do CME futures gaps fill? Most CME Bitcoin futures gaps eventually fill, though the timing varies. Some fill within days, while others may take weeks. Larger gaps typically take longer to close. Are CME gaps reliable trading signals? While CME gaps provide valuable information, they shouldn’t be used as standalone signals. Combine gap analysis with other technical indicators and market fundamentals for better accuracy. Can retail traders benefit from CME gaps? Absolutely. Both institutional and retail traders can develop strategies around CME Bitcoin futures gaps. The key is understanding the patterns and managing risk appropriately. How large was the largest recorded CME Bitcoin futures gap? The largest CME Bitcoin futures gaps have exceeded $2,000 during periods of extreme market volatility, though gaps typically range from $200 to $1,200. Do CME gaps affect Bitcoin’s spot price? Yes, CME Bitcoin futures gaps can influence spot prices as arbitrage opportunities emerge. However, the relationship works both ways, with spot market movements creating the gaps initially. Found this analysis helpful? Share this insight into CME Bitcoin futures gaps with fellow traders on social media and help others understand these important market patterns. Your sharing helps build a more informed trading community! To learn more about the latest Bitcoin trends, explore our article on key developments shaping Bitcoin price action and institutional adoption. This post Stunning CME Bitcoin Futures Gap: $960 Weekend Volatility Creates Massive Trading Opportunity first appeared on BitcoinWorld.
Share
Coinstats2025/11/10 08:25