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=trueIntroduction: Why OOP in Go Isn't What You ThinkI'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 racesMixing receiver types: ~35% of struct methodsCreating getters/setters for everything: ~60% of structsTrying to implement inheritance: ~40% of new Go developersAfter 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 DilemmaValue vs Pointer ReceiverThis is question #1 in interviews and code reviews. Here's a simple rule that covers 90% of cases:// Value receiver - for immutable methodsfunc (u User) FullName() string { return fmt.Sprintf("%s %s", u.FirstName, u.LastName)}// Pointer receiver - when changing statefunc (u *User) SetEmail(email string) error { if !isValidEmail(email) { return ErrInvalidEmail } u.Email = email u.UpdatedAt = time.Now() return nil}Rules for Choosing a Receivertype 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 receiversfunc (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 receiversfunc (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 ReceiverMethod modifies stateStruct contains mutex (otherwise it will be copied!)Large struct (avoid copying)Consistency (if at least one method requires pointer)// Struct with mutex ALWAYS pointer receivertype 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 receiverfunc (c *Cache) Get(key string) interface{} { c.mu.RLock() defer c.mu.RUnlock() return c.data[key]}Constructors and Factory FunctionsGo doesn't have constructors in the classical sense, but there's the New* idiom:// BAD: direct struct creationfunc main() { user := &User{ ID: generateID(), // What if we forget? Email: "test@test.com", // CreatedAt not set! }}// GOOD: factory function guarantees initializationfunc 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 PatternFor structs with many optional parameters:type Server struct { host string port int timeout time.Duration maxConns int tls *tls.Config}// Option - function that modifies Servertype Option func(*Server)// Factory functions for optionsfunc 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 optionsfunc 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 proseserver := NewServer("localhost", 8080, WithTimeout(60*time.Second), WithMaxConnections(1000), WithTLS(tlsConfig),)Encapsulation Through NamingGo 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 methodfunc (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 helperfunc (u *User) validatePassword(pwd string) error { return bcrypt.CompareHashAndPassword([]byte(u.password), []byte(pwd))}// Public method uses private onefunc (u *User) Authenticate(pwd string) error { if err := u.validatePassword(pwd); err != nil { return ErrInvalidCredentials } return nil}Composition Through EmbeddingInstead of inheritance, Go offers embedding. This is NOT inheritance, it's composition:// Base structtype 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 Persontype Employee struct { Person // Embedding - NOT inheritance! EmployeeID string Department string Salary decimal.Decimal}// Employee can override Person's methodsfunc (e Employee) FullName() string { return fmt.Sprintf("%s (%s)", e.Person.FullName(), e.EmployeeID)}// Usageemp := 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 methodfmt.Println(emp.Age()) // 34 - method from Personfmt.Println(emp.FirstName) // John - field from PersonEmbedding Interfacestype Reader interface { Read([]byte) (int, error)}type Writer interface { Write([]byte) (int, error)}// ReadWriter embeds both interfacestype ReadWriter interface { Reader Writer}// Struct can embed interfaces for delegationtype 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}// Usagevar buf bytes.Bufferlogged := 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 chainingfunc 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 SQLquery := 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 10Thread-Safe Structs// BAD: race conditiontype Counter struct { value int}func (c *Counter) Inc() { c.value++ // Race when accessed concurrently!}// GOOD: protected with mutextype 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 atomictype 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 Them1. Getters/Setters for All Fields// BAD: Java-style getters/setterstype 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 logictype 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 Objecttype 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 concernstype 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 TipsAlways use constructors for structs with invariantsBe consistent with receivers within a typePrefer composition over inheritance (which doesn't exist)Embedding is not inheritance, it's delegationProtect concurrent access with a mutex or channelsDon't create getters/setters without necessityStruct and Method ChecklistConstructor New* for complex initializationConsistent receivers (all pointer or all value)Pointer receiver for structs with a mutexPrivate fields for encapsulationEmbedding instead of inheritanceThread-safety when neededMinimal getters/settersConclusionStructs 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!