Mastering Error and Exception Handling in Go: Best Practices and Patterns
This article explains the distinction between errors and exceptions in Go, outlines when to use error returns versus panic/recover, and provides a series of practical patterns with code examples to write clearer, more maintainable backend code.
1. Introduction
Errors and exceptions are often confused; many developers treat every abnormal situation as an error and ignore the concept of exceptions. In Go, the language follows a "less is more" philosophy, only providing exception‑like mechanisms (panic/recover) when they add clear value.
2. Fundamentals
An error represents a problem that was expected, such as a failed file open, while an exception (panic) represents an unexpected condition, like a nil‑pointer dereference. Go uses the error interface for standard error handling, returning it as the last value in a function signature. The built‑in functions panic and recover trigger and handle exceptions, and defer postpones execution of a function until the surrounding function returns, regardless of whether it returns normally or via panic.
Errors and exceptions can be converted: an operation that fails repeatedly can promote an error to a panic, and a recovered panic can be turned back into an error for upstream handling.
3. Insight
In the regexp package, Compile returns (*Regexp, error) and is suitable for user‑provided patterns, while MustCompile panics on invalid input and is intended for hard‑coded patterns. The key takeaway is to define clear rules for when to express a problem as an error versus an exception, avoiding a "everything is an error" or "everything is an exception" approach.
4. Correct Practices
Practice 1: Use a boolean when there is only one failure reason
func (self *AgentContext) CheckHostType(host_type string) error {
switch host_type {
case "virtual_machine":
return nil
case "bare_metal":
return nil
}
return errors.New("CheckHostType ERROR:" + host_type)
}
func (self *AgentContext) IsValidHostType(hostType string) bool {
return hostType == "virtual_machine" || hostType == "bare_metal"
}If a function can fail for only one reason, return a bool instead of an error. When multiple failure reasons exist, keep returning error.
Practice 2: Omit error when there is no failure
func (self *CniParam) setTenantId() {
self.TenantId = self.PodNs
}
self.setTenantId() // no error handling neededPractice 3: Place error as the last return value
resp, err := http.Get(url)
if err != nil {
return nil, err
}
value, ok := cache.Lookup(key)
if !ok {
// handle missing value
}Practice 4: Define error constants centrally
var ERR_EOF = errors.New("EOF")
var ERR_CLOSED_PIPE = errors.New("io: read/write on closed pipe")
var ERR_NO_PROGRESS = errors.New("multiple Read calls return no data or error")
var ERR_SHORT_BUFFER = errors.New("short buffer")
var ERR_SHORT_WRITE = errors.New("short write")
var ERR_UNEXPECTED_EOF = errors.New("unexpected EOF")Practice 5: Log at every layer when propagating errors
Adding logs at each layer simplifies fault diagnosis.
Practice 6: Use defer for cleanup on error paths
func deferDemo() error {
err := createResource1()
if err != nil {
return ERR_CREATE_RESOURCE1_FAILED
}
defer func() {
if err != nil {
destroyResource1()
}
}()
// repeat for other resources
return nil
}Practice 7: Retry transient failures instead of returning immediately
When failures are occasional, retry the operation a limited number of times with back‑off before propagating an error.
Practice 8: Do not return error from cleanup functions that callers ignore
For functions like destroy or clear, log the error internally and omit it from the signature.
Practice 9: Preserve useful return values when an error occurs
If a function returns useful data alongside a non‑nil error (e.g., Read returns bytes read), handle both values.
5. Exception Handling Practices
Practice 1: Fail fast during development
Use panic to surface bugs early, ensuring they are noticed and fixed promptly.
Practice 2: Recover in production to avoid process termination
Wrap top‑level goroutine code with a deferred recover that logs the stack and converts the panic into an error so the program can continue safely.
func funcA() (err error) {
defer func() {
if p := recover(); p != nil {
fmt.Printf("panic recover! p: %v", p)
debug.PrintStack()
if str, ok := p.(string); ok {
err = errors.New(str)
} else {
err = errors.New("panic")
}
}
}()
return funcB()
}Practice 3: Use panic for impossible branches
switch s := suit(drawCard()); s {
case "Spades":
// ...
case "Hearts":
// ...
case "Diamonds":
// ...
case "Clubs":
// ...
default:
panic(fmt.Sprintf("invalid suit %v", s))
}Practice 4: Panic when input should never be invalid (hard‑coded scenarios)
func MustCompile(str string) *Regexp {
re, err := Compile(str)
if err != nil {
panic("regexp: Compile(`" + quote(str) + "`): " + err.Error())
}
return re
}6. Conclusion
The article uses Go as an example to clarify the difference between errors and exceptions and presents a collection of practical handling patterns that can be applied individually or combined to improve code readability and maintainability.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Go Development Architecture Practice
Daily sharing of Golang-related technical articles, practical resources, language news, tutorials, real-world projects, and more. Looking forward to growing together. Let's go!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
