- Keep functions under 50 lines (The Screen Rule) - One function = one responsibility - Use early returns instead of nested if statements - Always wrap errors with context using `fmt.Errorf` and `%w` - Use `defer` for guaranteed resource cleanup - Name functions with verb + noun pattern Jump to the checklist for quick reference.- Keep functions under 50 lines (The Screen Rule) - One function = one responsibility - Use early returns instead of nested if statements - Always wrap errors with context using `fmt.Errorf` and `%w` - Use `defer` for guaranteed resource cleanup - Name functions with verb + noun pattern Jump to the checklist for quick reference.

Clean Code: Functions and Error Handling in Go: From Chaos to Clarity [Part 1]

2025/10/31 14:30

Introduction: Why Go Functions Are Special

I've reviewed over 1000 pull requests in Go over the past 6 years, and the same mistakes keep appearing. Remember your first Go code? It probably had dozens of if err != nil checks and 200-line functions that did everything at once. After analyzing over 50 Go projects, I've identified the main beginner problem: they write Go like Java or Python, ignoring the language's idioms.

Common function problems I've seen:

  • Functions over 100 lines: ~40% of codebases
  • Mixed responsibilities: ~60% of functions
  • Poor error handling: ~30% of bugs
  • Missing defer for cleanup: ~45% of resource leaks

In this article — the first in a Clean Code in Go series — we'll explore how to write functions you won't be ashamed to show in code review. We'll discuss the single responsibility principle, error handling, and why defer is your best friend.

Single Responsibility Principle: One Function — One Job

Here's a typical function from a real project (names changed):

// BAD: monster function does everything func ProcessUserData(userID int) (*User, error) { // Validation if userID <= 0 { log.Printf("Invalid user ID: %d", userID) return nil, errors.New("invalid user ID") } // Database connection db, err := sql.Open("postgres", connString) if err != nil { log.Printf("DB connection failed: %v", err) return nil, err } defer db.Close() var user User err = db.QueryRow("SELECT * FROM users WHERE id = $1", userID).Scan(&user.ID, &user.Name, &user.Email) if err != nil { log.Printf("Query failed: %v", err) return nil, err } // Data enrichment if user.Email != "" { domain := strings.Split(user.Email, "@")[1] user.EmailDomain = domain // Check corporate domain corporateDomains := []string{"google.com", "microsoft.com", "apple.com"} for _, corp := range corporateDomains { if domain == corp { user.IsCorporate = true break } } } // Logging log.Printf("User %d processed successfully", userID) return &user, nil }

This function violates SRP on multiple fronts:

  • Validates input data
  • Manages database connections
  • Executes queries
  • Enriches data
  • Handles logging

The Screen Rule

Quality metric: A function should fit entirely on a developer's screen (roughly 30-50 lines). If you need to scroll — time to refactor.

Let's refactor following Go idioms:

// GOOD: each function has one responsibility func GetUser(ctx context.Context, userID int) (*User, error) { if err := validateUserID(userID); err != nil { return nil, fmt.Errorf("validation failed: %w", err) } user, err := fetchUserFromDB(ctx, userID) if err != nil { return nil, fmt.Errorf("fetch user %d: %w", userID, err) } enrichUserData(user) return user, nil } func validateUserID(id int) error { if id <= 0 { return fmt.Errorf("invalid user ID: %d", id) } return nil } func fetchUserFromDB(ctx context.Context, userID int) (*User, error) { row := db.QueryRowContext(ctx, ` SELECT id, name, email FROM users WHERE id = $1`, userID) var user User if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrUserNotFound } return nil, err } return &user, nil } func enrichUserData(user *User) { if user.Email == "" { return } parts := strings.Split(user.Email, "@") if len(parts) != 2 { return } user.EmailDomain = parts[1] user.IsCorporate = isCorporateDomain(user.EmailDomain) }

Now each function:

  • Fits on screen (20 lines max)
  • Has single responsibility
  • Can be tested independently

Error Handling: The Go Way

Problem: Nested Hell

Beginners often create the "pyramid of doom":

// BAD: deep nesting func SendNotification(userID int, message string) error { user, err := GetUser(userID) if err == nil { if user.Email != "" { if user.IsActive { if user.NotificationsEnabled { err := smtp.Send(user.Email, message) if err == nil { log.Printf("Sent to %s", user.Email) return nil } else { log.Printf("Failed to send: %v", err) return err } } else { return errors.New("notifications disabled") } } else { return errors.New("user inactive") } } else { return errors.New("email empty") } } else { return fmt.Errorf("user not found: %v", err) } }

Solution: Early Return (Guard Clauses)

// GOOD: early return on errors func SendNotification(userID int, message string) error { user, err := GetUser(userID) if err != nil { return fmt.Errorf("get user %d: %w", userID, err) } if user.Email == "" { return ErrEmptyEmail } if !user.IsActive { return ErrUserInactive } if !user.NotificationsEnabled { return ErrNotificationsDisabled } if err := smtp.Send(user.Email, message); err != nil { return fmt.Errorf("send to %s: %w", user.Email, err) } log.Printf("Notification sent to %s", user.Email) return nil }

Error Wrapping: Context Matters

Since Go 1.13, fmt.Errorf with the %w verb wraps errors. Always use it:

// Define sentinel errors for business logic var ( ErrUserNotFound = errors.New("user not found") ErrInsufficientFunds = errors.New("insufficient funds") ErrOrderAlreadyProcessed = errors.New("order already processed") ) func ProcessPayment(orderID string) error { order, err := fetchOrder(orderID) if err != nil { // Add context to the error return fmt.Errorf("process payment for order %s: %w", orderID, err) } if order.Status == "processed" { return ErrOrderAlreadyProcessed } if err := chargeCard(order); err != nil { // Wrap technical errors return fmt.Errorf("charge card for order %s: %w", orderID, err) } return nil } // Calling code can check error type if err := ProcessPayment("ORD-123"); err != nil { if errors.Is(err, ErrOrderAlreadyProcessed) { // Business logic for already processed order return nil } if errors.Is(err, ErrInsufficientFunds) { // Notify user about insufficient funds notifyUser(err) } // Log unexpected errors log.Printf("Payment failed: %v", err) return err }

Defer: Guaranteed Resource Cleanup

defer is one of Go's killer features. Use it for guaranteed cleanup:

// BAD: might forget to release resources func ReadConfig(path string) (*Config, error) { file, err := os.Open(path) if err != nil { return nil, err } data, err := io.ReadAll(file) if err != nil { file.Close() // Easy to forget during refactoring return nil, err } var config Config if err := json.Unmarshal(data, &config); err != nil { file.Close() // Duplication return nil, err } file.Close() // And again return &config, nil }

// GOOD: defer guarantees closure func ReadConfig(path string) (*Config, error) { file, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open config %s: %w", path, err) } defer file.Close() // Will execute no matter what data, err := io.ReadAll(file) if err != nil { return nil, fmt.Errorf("read config %s: %w", path, err) } var config Config if err := json.Unmarshal(data, &config); err != nil { return nil, fmt.Errorf("parse config %s: %w", path, err) } return &config, nil }

Pattern: Cleanup Functions

func WithTransaction(ctx context.Context, fn func(*sql.Tx) error) error { tx, err := db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin transaction: %w", err) } // defer executes in LIFO order defer func() { if p := recover(); p != nil { tx.Rollback() panic(p) // re-throw panic after cleanup } if err != nil { tx.Rollback() } else { err = tx.Commit() } }() err = fn(tx) return err } // Usage err := WithTransaction(ctx, func(tx *sql.Tx) error { // All logic in transaction // Rollback/Commit happens automatically return nil })

Practical Tips

1. Function Naming

// BAD: unclear purpose func Process(data []byte) error func Handle(r Request) Response func Do() error // GOOD: verb + noun func ParseJSON(data []byte) (*Config, error) func ValidateEmail(email string) error func SendNotification(user *User, msg string) error

2. Function Parameters

If more than 3-4 parameters — use a struct:

// BAD: too many parameters func CreateUser(name, email, phone, address string, age int, isActive bool) (*User, error) // GOOD: group into struct type CreateUserRequest struct { Name string Email string Phone string Address string Age int IsActive bool } func CreateUser(req CreateUserRequest) (*User, error)

3. Return Values

// BAD: boolean flags are unclear func CheckPermission(userID int) (bool, bool, error) // what does first bool mean? second? // GOOD: use named returns or struct func CheckPermission(userID int) (canRead, canWrite bool, err error) // BETTER: struct for complex results type Permissions struct { CanRead bool CanWrite bool CanDelete bool } func CheckPermission(userID int) (*Permissions, error)

Clean Function Checklist

  • Fits on screen (30-50 lines max)
  • Does one thing (Single Responsibility)
  • Has clear name (verb + noun)
  • Uses early return for errors
  • Wraps errors with context (%w)
  • Uses defer for cleanup
  • Accepts context if can be cancelled
  • No side effects (or clearly documented)

Conclusion

Clean functions in Go aren't just about following general Clean Code principles. It's about understanding and using language idioms: early return instead of nesting, error wrapping for context, defer for guaranteed cleanup.

In the next article, we'll discuss structs and methods: when to use value vs pointer receivers, how to organize composition properly, and why embedding isn't inheritance.

What's your approach to keeping functions clean? Do you have a maximum line limit for your team? Let me know 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 [email protected] 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.
Share Insights

You May Also Like

Astonishing Kevin Durant Bitcoin Fortune: A Decade-Long Hold Yields 195-Fold Return

Astonishing Kevin Durant Bitcoin Fortune: A Decade-Long Hold Yields 195-Fold Return

BitcoinWorld Astonishing Kevin Durant Bitcoin Fortune: A Decade-Long Hold Yields 195-Fold Return Imagine logging into an old account and discovering a fortune! That’s exactly what happened to NBA superstar Kevin Durant. His decade-old, forgotten Coinbase account, which held an early Kevin Durant Bitcoin investment, has now resurfaced, revealing an incredible 195-fold return. This remarkable story highlights the immense potential of long-term cryptocurrency holdings and serves as a fascinating example for anyone interested in digital assets. The Accidental ‘Hodl’: How Kevin Durant’s Bitcoin Investment Skyrocketed The journey of Kevin Durant’s Bitcoin investment began in 2016. He encountered Bitcoin, then priced at a modest $600, during a birthday celebration for venture capitalist Ben Horowitz. Intrigued, Durant decided to invest, setting up a Coinbase account. However, as many early adopters can attest, managing digital assets in the nascent crypto landscape wasn’t always straightforward. Durant subsequently misplaced his Coinbase login credentials, leading to an involuntary long-term hold – a phenomenon affectionately known as "HODL" (Hold On for Dear Life) in the crypto community. This accidental strategy proved to be a stroke of pure luck. After a decade, with assistance from Coinbase and a thorough identity verification process, Durant successfully recovered his account. While the exact amount of BTC remains undisclosed, the outcome is clear: a staggering 195-fold return on his initial investment. Initial Investment: Bitcoin at $600 in 2016. Accidental Strategy: Lost login details led to an unintentional "HODL." Recovery: Coinbase assisted with identity verification. Return: A remarkable 195-fold increase in value. Beyond Personal Gains: Kevin Durant’s Broader Crypto Engagement This isn’t Kevin Durant’s first foray into the world of digital assets, nor is it his only connection to the industry. Long before this incredible recovery, Durant had already demonstrated a positive and forward-thinking stance toward cryptocurrency. His engagement extends beyond just holding assets; he has actively participated in the crypto ecosystem. Durant previously partnered with Coinbase, one of the leading cryptocurrency exchanges, showcasing his belief in the platform and the broader potential of digital currencies. He has also ventured into the realm of Non-Fungible Tokens (NFTs), purchasing digital collectibles and exploring this evolving sector. These actions underscore his understanding and acceptance of crypto’s growing influence. His continued involvement helps bridge the gap between mainstream culture and the crypto world, bringing increased visibility and legitimacy to digital assets. The story of his Kevin Durant Bitcoin recovery only adds another layer to his impressive crypto narrative, inspiring many to consider the long-term prospects of digital investments. Valuable Lessons from Kevin Durant’s Bitcoin Journey Kevin Durant’s story offers compelling insights for both seasoned investors and newcomers to the crypto space. It powerfully illustrates the potential rewards of a patient, long-term investment approach, even if accidental. While not everyone will forget their login details for a decade, the principle of "HODLing" through market volatility can yield significant returns. However, it also subtly highlights the importance of proper security and record-keeping. Losing access to an account, even if eventually recovered, can be a stressful experience. Here are some actionable takeaways: Embrace Long-Term Vision: Bitcoin’s history shows substantial growth over extended periods. Patience often outperforms short-term trading. Secure Your Assets: Always keep your login details, seed phrases, and recovery information in multiple, secure locations. Consider hardware wallets for significant holdings. Understand the Volatility: Crypto markets are volatile. Investing only what you can afford to lose and being prepared for price swings is crucial. Stay Informed: While Durant’s hold was accidental, continuous learning about the crypto market can help make informed decisions. His experience reinforces the idea that strategic, even if involuntary, patience can be profoundly rewarding in the world of cryptocurrency. The Kevin Durant Bitcoin story is a testament to this. The tale of Kevin Durant’s forgotten Coinbase account and his astonishing 195-fold return on a decade-old Bitcoin investment is nothing short of extraordinary. It’s a vivid reminder of the transformative power of early adoption and the incredible growth potential within the cryptocurrency market. Beyond the personal windfall, Durant’s continued engagement with crypto, from partnerships to NFTs, reinforces his role as a prominent figure in the digital asset space. His accidental "HODL" has become a legendary example, inspiring many to look at long-term crypto investments with renewed optimism and a keen eye on future possibilities. Frequently Asked Questions About Kevin Durant’s Bitcoin Investment Here are some common questions regarding Kevin Durant’s recent crypto revelation: Q: How much did Kevin Durant initially invest in Bitcoin?A: The exact amount of Bitcoin Kevin Durant initially invested has not been disclosed. However, it was purchased around 2016 when Bitcoin was priced at approximately $600. Q: How did Kevin Durant recover his forgotten Coinbase account?A: Coinbase assisted Kevin Durant in recovering his account after he completed a thorough identity verification process, confirming his ownership of the decade-old account. Q: What does "195-fold return" mean?A: A "195-fold return" means that the value of his initial investment multiplied by 195 times. If he invested $1,000, it would now be worth $195,000. Q: Has Kevin Durant invested in other cryptocurrencies or NFTs?A: Yes, Kevin Durant has shown a friendly stance toward cryptocurrency beyond Bitcoin. He has partnered with Coinbase and has also purchased Non-Fungible Tokens (NFTs) in the past. Q: Is Kevin Durant’s story typical for Bitcoin investors?A: While the 195-fold return is exceptional, the principle of significant gains from long-term holding (HODLing) is a common theme in Bitcoin’s history. However, not all investments yield such high returns, and market volatility is always a factor. Did Kevin Durant’s incredible crypto journey inspire you? Share this astonishing story with your friends and followers on social media to spark conversations about the future of digital assets and the power of long-term investing! Your shares help us bring more fascinating crypto news to a wider audience. To learn more about the latest Bitcoin trends, explore our article on key developments shaping Bitcoin’s institutional adoption. This post Astonishing Kevin Durant Bitcoin Fortune: A Decade-Long Hold Yields 195-Fold Return first appeared on BitcoinWorld.
Share
Coinstats2025/09/19 18:45