Chuyển đến nội dung chính

Lesson 3: Functions, Interfaces & Error Handling

Functions, multiple return values, variadic functions. Interfaces, embedding, composition over inheritance. Error handling patterns, custom errors, errors.Is/As, panic/recover.

💻 Programming — Lesson 3 Lesson 3: Functions, Interfaces & Errors Handling

Golang: From Basics to Advanced

Part 1: Go Fundamentals

xdev.asia

1. Functions in Go

Functions are the basic building block in Go. Go functions have many unique features compared to other languages.

1.1. Basic Functions

package main

import "fmt"

// Function cơ bản
func greet(name string) string {
    return "Hello, " + name + "!"
}

// Multiple parameters cùng type
func add(a, b int) int {
    return a + b
}

// Multiple return values - đặc trưng Go
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

// Named return values
func swap(a, b string) (first, second string) {
    first = b
    second = a
    return // naked return - trả named values
}

func main() {
    fmt.Println(greet("Go"))       // "Hello, Go!"
    fmt.Println(add(3, 5))         // 8
    
    result, err := divide(10, 3)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(result)            // 3.333...
    
    a, b := swap("hello", "world")
    fmt.Println(a, b)              // "world" "hello"
}

1.2. Variadic Functions

// Variadic function - nhận số lượng tham số bất kỳ
func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// Mix fixed và variadic params
func printf(format string, args ...interface{}) {
    fmt.Printf(format, args...)
}

func main() {
    fmt.Println(sum(1, 2, 3))         // 6
    fmt.Println(sum(1, 2, 3, 4, 5))   // 15
    
    // Spread slice
    nums := []int{10, 20, 30}
    fmt.Println(sum(nums...))          // 60
}

1.3. First-class Functions

// Function as variable
var multiply func(int, int) int = func(a, b int) int {
    return a * b
}

// Function as parameter (callback)
func apply(a, b int, op func(int, int) int) int {
    return op(a, b)
}

// Function as return value (closure)
func makeCounter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

// Function type alias
type MathFunc func(int, int) int

func calculate(a, b int, fn MathFunc) int {
    return fn(a, b)
}

func main() {
    // Anonymous function (lambda)
    double := func(x int) int {
        return x * 2
    }
    fmt.Println(double(5)) // 10
    
    // Callback
    fmt.Println(apply(3, 4, multiply))  // 12
    fmt.Println(apply(10, 3, func(a, b int) int {
        return a - b
    }))                                  // 7
    
    // Closure
    counter := makeCounter()
    fmt.Println(counter()) // 1
    fmt.Println(counter()) // 2
    fmt.Println(counter()) // 3
    
    // IIFE (Immediately Invoked Function Expression)
    result := func(x int) int {
        return x * x
    }(5)
    fmt.Println(result) // 25
}

1.4. Defer

import "os"

// defer: thực thi khi function return (LIFO order)
func readFile(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close() // Đảm bảo file luôn được close
    
    // ... đọc file
    return nil
}

func main() {
    // Defer chạy theo LIFO (Last In, First Out)
    defer fmt.Println("1")
    defer fmt.Println("2")
    defer fmt.Println("3")
    // Output: 3, 2, 1
    
    // Use case phổ biến: cleanup resources
    // defer mutex.Unlock()
    // defer rows.Close()
    // defer tx.Rollback()
    // defer span.End()
}

2. Interfaces

Interfaces in Go are implicit.implicit — type only needs to implement methods, no need to declare "implements". This is a big difference compared to Java/C#.

2.1. Interface definition

// Interface definition
type Shape interface {
    Area() float64
    Perimeter() float64
}

// Circle implements Shape implicitly
type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Rectangle implements Shape implicitly
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

// Hàm nhận interface
func printShapeInfo(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

func main() {
    c := Circle{Radius: 5}
    r := Rectangle{Width: 10, Height: 3}
    
    printShapeInfo(c)  // Area: 78.54, Perimeter: 31.42
    printShapeInfo(r)  // Area: 30.00, Perimeter: 26.00
    
    // Slice of interfaces
    shapes := []Shape{c, r}
    for _, s := range shapes {
        printShapeInfo(s)
    }
}

2.2. Common Interfaces in Go

// io.Reader - interface quan trọng nhất
type Reader interface {
    Read(p []byte) (n int, err error)
}

// io.Writer
type Writer interface {
    Write(p []byte) (n int, err error)
}

// fmt.Stringer (tương đương toString() trong Java)
type Stringer interface {
    String() string
}

// error interface
type error interface {
    Error() string
}

// sort.Interface
type Interface interface {
    Len() int
    Less(i, j int) bool
    Swap(i, j int)
}

// Implement Stringer
type User struct {
    Name string
    Age  int
}

func (u User) String() string {
    return fmt.Sprintf("%s (age %d)", u.Name, u.Age)
}

func main() {
    user := User{Name: "Alice", Age: 30}
    fmt.Println(user) // "Alice (age 30)" - tự gọi String()
}

2.3. Interface Composition (Embedding)

// Compose interfaces từ các interfaces nhỏ
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

type Closer interface {
    Close() error
}

// Composed interface
type ReadWriter interface {
    Reader
    Writer
}

type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

// Best practice: Keep interfaces SMALL
// Go proverb: "The bigger the interface, the weaker the abstraction"
// Thường chỉ 1-3 methods

2.4. Empty Interface & any

// interface{} (Go < 1.18) hoặc any (Go 1.18+) chấp nhận mọi type
func printAnything(v any) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}

func main() {
    printAnything(42)           // Type: int, Value: 42
    printAnything("hello")      // Type: string, Value: hello
    printAnything(true)         // Type: bool, Value: true
    printAnything([]int{1, 2})  // Type: []int, Value: [1 2]
    
    // map[string]any - thường dùng cho JSON
    data := map[string]any{
        "name":  "Alice",
        "age":   30,
        "roles": []string{"admin", "user"},
    }
    fmt.Println(data)
}

3. Error Handling

Go has no exceptions (try-catch). Error handling used explicit return values — this is an intentional design choice.

3.1. Basic Error Handling

import (
    "errors"
    "fmt"
    "os"
    "strconv"
)

// Hàm trả về (result, error)
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// fmt.Errorf với format string
func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("invalid user ID: %d", id)
    }
    // ... lookup user
    return &User{Name: "Alice"}, nil
}

func main() {
    // Pattern: check error immediately
    result, err := divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(result)
    
    // Chaining error checks
    f, err := os.Open("config.json")
    if err != nil {
        fmt.Println("Cannot open file:", err)
        return
    }
    defer f.Close()
    
    // strconv error example
    num, err := strconv.Atoi("abc")
    if err != nil {
        fmt.Println("Parse error:", err)
    }
    fmt.Println(num)
}

3.2. Custom Errors

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}

// Sentinel errors (pre-defined errors)
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrForbidden    = errors.New("forbidden")
)

func getUser(id int) (*User, error) {
    if id <= 0 {
        return nil, &ValidationError{
            Field:   "id",
            Message: "must be positive",
        }
    }
    // ... user not in DB
    return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
}

3.3. Error Wrapping (Go 1.13+)

import "errors"

func readConfig() error {
    f, err := os.Open("config.yaml")
    if err != nil {
        // %w wraps the original error
        return fmt.Errorf("readConfig: %w", err)
    }
    defer f.Close()
    
    // ... parse
    return nil
}

func initApp() error {
    if err := readConfig(); err != nil {
        return fmt.Errorf("initApp: %w", err)
    }
    return nil
}

func main() {
    err := initApp()
    if err != nil {
        fmt.Println(err)
        // "initApp: readConfig: open config.yaml: no such file or directory"
        
        // errors.Is - check nếu error chain chứa target error
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("File does not exist")
        }
        
        // errors.As - extract specific error type từ chain
        var pathErr *os.PathError
        if errors.As(err, &pathErr) {
            fmt.Println("Path:", pathErr.Path)
            fmt.Println("Op:", pathErr.Op)
        }
    }
}

3.4. Panic & Recover

// panic: dùng cho unrecoverable errors (thay vì throw)
// recover: bắt panic (thay vì catch)

func riskyOperation() {
    // panic dừng execution, unwind stack
    panic("something went terribly wrong")
}

// recover chỉ hoạt động trong defer
func safeOperation() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from panic:", r)
        }
    }()
    
    riskyOperation()
    fmt.Println("This won't print")
}

func main() {
    safeOperation()
    fmt.Println("Program continues normally")
}

// ⚠️ Quy tắc:
// - KHÔNG dùng panic cho business logic errors
// - Dùng panic khi: programmer error, unrecoverable state
// - recover thường dùng trong middleware (HTTP handler recovery)

3.5. Error Handling Patterns are commonly used

// Pattern 1: Early return
func processOrder(order *Order) error {
    if order == nil {
        return errors.New("order is nil")
    }
    if order.Total <= 0 {
        return fmt.Errorf("invalid total: %.2f", order.Total)
    }
    if err := validateItems(order.Items); err != nil {
        return fmt.Errorf("validate items: %w", err)
    }
    if err := chargePayment(order); err != nil {
        return fmt.Errorf("charge payment: %w", err)
    }
    return nil
}

// Pattern 2: Error type assertion for flow control
func handleError(err error) {
    var validErr *ValidationError
    if errors.As(err, &validErr) {
        // 400 Bad Request
        fmt.Printf("Validation: %s - %s\n", validErr.Field, validErr.Message)
        return
    }
    if errors.Is(err, ErrNotFound) {
        // 404 Not Found
        fmt.Println("Resource not found")
        return
    }
    if errors.Is(err, ErrUnauthorized) {
        // 401 Unauthorized
        fmt.Println("Please login")
        return
    }
    // 500 Internal Server Error
    fmt.Println("Internal error:", err)
}

// Pattern 3: errors.Join (Go 1.20+) - multiple errors
func validateUser(u *User) error {
    var errs []error
    if u.Name == "" {
        errs = append(errs, errors.New("name is required"))
    }
    if u.Email == "" {
        errs = append(errs, errors.New("email is required"))
    }
    if u.Age < 0 {
        errs = append(errs, errors.New("age must be non-negative"))
    }
    return errors.Join(errs...)
}

4. Structure Embedding (Composition)

// Go dùng composition thay vì inheritance
type BaseModel struct {
    ID        int
    CreatedAt time.Time
    UpdatedAt time.Time
}

type User struct {
    BaseModel          // Embedding - "kế thừa" tất cả fields và methods
    Name      string
    Email     string
}

type Post struct {
    BaseModel
    Title   string
    Content string
    Author  User
}

func main() {
    user := User{
        BaseModel: BaseModel{
            ID:        1,
            CreatedAt: time.Now(),
        },
        Name:  "Alice",
        Email: "[email protected]",
    }
    
    // Access embedded fields trực tiếp
    fmt.Println(user.ID)        // 1 (từ BaseModel)
    fmt.Println(user.Name)      // "Alice"
    fmt.Println(user.CreatedAt) // (từ BaseModel)
}

5. Summary

  • Functions: Multiple return values, first-class, closures, defer
  • Interfaces: Implicit implementation, small interfaces, composition
  • Error handling: Explicit errors, wrapping (%w), errors.Is/As, custom errors
  • Composition: Structure embedding instead of inheritance
  • Panic/Recover: Only for unrecoverable errors, not for business logic

Next article: Generics, Packages & Modules — type parameters system and code management in Go.