Skip to content

Error Handling & Defer

Python's try/except mapped to Go's if err != nil pattern, and related error handling constructs.

try / except

try:
    data = json.loads(raw)
except json.JSONDecodeError as e:
    print(f"Parse error: {e}")
var data map[string]any
err := json.Unmarshal([]byte(raw), &data)
if err != nil {
    fmt.Printf("Parse error: %v\n", err)
}

Go has no exceptions

Errors are return values. Callers must check err. Ignoring errors is an anti-pattern in Go.

raise

def divide(a, b):
    if b == 0:
        raise ValueError("division by zero")
    return a / b
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

result, err := divide(10, 0)
if err != nil {
    log.Fatal(err)
}

Multiple Exceptions

try:
    result = do_something()
except FileNotFoundError:
    print("File not found")
except PermissionError:
    print("Permission denied")
except Exception as e:
    print(f"Other error: {e}")
err := doSomething()
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("File not found")
} else if errors.Is(err, os.ErrPermission) {
    fmt.Println("Permission denied")
} else if err != nil {
    fmt.Printf("Other error: %v\n", err)
}

finally

f = open("file.txt")
try:
    data = f.read()
finally:
    f.close()
// "defer" ensures a function runs when the surrounding function returns
f, err := os.Open("file.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close()  // runs automatically when the function exits

data, err := io.ReadAll(f)

defer is LIFO (last in, first out)

Multiple defer calls execute in reverse order:

defer fmt.Println("first")
defer fmt.Println("second")
defer fmt.Println("third")
// Output: third, second, first

Context Manager

with open("file.txt") as f:
    data = f.read()
# f is automatically closed
f, err := os.Open("file.txt")
if err != nil {
    log.Fatal(err)
}
defer f.Close()

data, err := io.ReadAll(f)

Custom Exception

class NotFoundError(Exception):
    def __init__(self, name: str):
        self.name = name
        super().__init__(f"{name} not found")

raise NotFoundError("user")
type NotFoundError struct {
    Name string
}

// Implement the error interface
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", e.Name)
}

func findUser(name string) error {
    return &NotFoundError{Name: name}
}

// Use errors.As to extract the concrete error type
var nfe *NotFoundError
if errors.As(err, &nfe) {
    fmt.Printf("Missing: %s\n", nfe.Name)
}

panic/recover is not normal error handling

Go has panic and recover, but they are reserved for truly unrecoverable errors (e.g., programmer bugs). Normal error handling always uses return error.