Back to blog

Go Phase 2: Core Concepts - Kiểu Dữ Liệu, Interface & Error Handling

gogolangprogrammingbackendfundamentals
Go Phase 2: Core Concepts - Kiểu Dữ Liệu, Interface & Error Handling

Chào mừng bạn đến với Phase 2 của Go Learning Roadmap! Ở Phase 1, bạn đã học cú pháp Go — biến, hàm, luồng điều khiển, slices, maps và structs. Giờ là lúc học những concept giúp chương trình Go có cấu trúc tốt: cách tổ chức behavior với methods và interfaces, xử lý lỗi đúng cách, và chia code thành packages.

Đây là nền tảng của mọi dự án Go thực tế. Nắm vững chúng ở đây, Phase 3 (concurrency) sẽ dễ hiểu hơn rất nhiều.

Bạn Sẽ Học Được Gì

✅ Định nghĩa methods trên structs — value receiver vs pointer receiver
✅ Dùng interfaces cho polymorphism — mô hình implicit implementation của Go
✅ Xử lý lỗi đúng cách — error interface, custom errors, wrapping
✅ Tổ chức code thành packages — exported vs unexported, quy ước đặt tên
✅ Quản lý dependencies với Go modules — go.mod, go get, go mod tidy
✅ Làm việc với standard library — fmt, strings, strconv, os, io


Phần 1: Structs và Methods

Bạn đã gặp structs ở Phase 1. Giờ đi sâu hơn — structs là cách chính trong Go để định nghĩa custom types và gắn behavior.

Định Nghĩa Structs

type User struct {
    ID        int
    FirstName string
    LastName  string
    Email     string
    Age       int
    Active    bool
}
 
// Tạo instance
u1 := User{ID: 1, FirstName: "Alice", LastName: "Smith", Email: "[email protected]", Age: 30, Active: true}
u2 := User{FirstName: "Bob"} // Zero values cho field chưa set: ID=0, Age=0, Active=false

Methods

Method là function có receiver — gắn liền với một type:

// Method trên User — value receiver
func (u User) FullName() string {
    return u.FirstName + " " + u.LastName
}
 
// Method trên User — pointer receiver (có thể sửa struct)
func (u *User) Deactivate() {
    u.Active = false
}
 
func main() {
    user := User{FirstName: "Alice", LastName: "Smith", Active: true}
    fmt.Println(user.FullName())  // "Alice Smith"
 
    user.Deactivate()
    fmt.Println(user.Active)      // false
}

Value Receiver vs Pointer Receiver

Đây là một trong những quyết định quan trọng nhất khi viết Go:

Khía CạnhValue Receiver (u User)Pointer Receiver (u *User)
Sửa bản gốc?Không — làm việc trên bản saoCó — sửa trực tiếp bản gốc
Dùng khiMethod chỉ đọc, struct nhỏMethod thay đổi state, struct lớn
Quy ướcGetters, string formattingSetters, thay đổi trạng thái
type Counter struct {
    count int
}
 
// Value receiver — trả giá trị, không sửa bản gốc
func (c Counter) Value() int {
    return c.count
}
 
// Pointer receiver — sửa struct gốc
func (c *Counter) Increment() {
    c.count++
}
 
func main() {
    c := Counter{}
    c.Increment()
    c.Increment()
    fmt.Println(c.Value()) // 2
}

Quy tắc: Nếu bất kỳ method nào dùng pointer receiver, tất cả method trên type đó nên dùng pointer receiver. Để tìm hiểu sâu hơn, xem Pointer Receiver vs Value Receiver.

Struct Embedding (Composition)

Go không có inheritance. Thay vào đó, bạn embed struct vào struct khác để tái sử dụng fields và methods:

type Address struct {
    Street string
    City   string
    State  string
    Zip    string
}
 
func (a Address) Format() string {
    return fmt.Sprintf("%s, %s, %s %s", a.Street, a.City, a.State, a.Zip)
}
 
type Employee struct {
    Name    string
    Role    string
    Address // Embedded — Employee "kế thừa" fields và methods của Address
}
 
func main() {
    e := Employee{
        Name: "Alice",
        Role: "Engineer",
        Address: Address{
            Street: "123 Main St",
            City:   "Portland",
            State:  "OR",
            Zip:    "97201",
        },
    }
 
    // Truy cập trực tiếp embedded fields
    fmt.Println(e.City)      // "Portland" — promoted từ Address
    fmt.Println(e.Format())  // "123 Main St, Portland, OR 97201" — promoted method
}

Điểm quan trọng: Embedding là composition, không phải inheritance. Employee có Address, không là Address. Xem thêm Favor Composition Over Inheritance.


Phần 2: Interfaces

Interface là công cụ trừu tượng mạnh nhất của Go — và hoạt động khác hoàn toàn so với Java hay TypeScript.

Implicit Implementation

Trong Go, một type implement interface tự động nếu nó có tất cả methods cần thiết. Không cần keyword implements:

// Định nghĩa interface
type Writer interface {
    Write(data []byte) (int, error)
}
 
// FileWriter implement Writer — không cần khai báo gì
type FileWriter struct {
    path string
}
 
func (fw FileWriter) Write(data []byte) (int, error) {
    fmt.Printf("Writing %d bytes to %s\n", len(data), fw.path)
    return len(data), nil
}
 
// ConsoleWriter cũng implement Writer
type ConsoleWriter struct{}
 
func (cw ConsoleWriter) Write(data []byte) (int, error) {
    fmt.Print(string(data))
    return len(data), nil
}
 
// Function nhận bất kỳ thứ gì implement Writer
func SaveReport(w Writer, report string) error {
    _, err := w.Write([]byte(report))
    return err
}
 
func main() {
    // Cả hai đều hoạt động — Go kiểm tra lúc compile time
    SaveReport(FileWriter{path: "/tmp/report.txt"}, "Q4 Results")
    SaveReport(ConsoleWriter{}, "Q4 Results")
}

Không có keyword implements. Nếu methods khớp, type tự động satisfy interface. Đây gọi là structural typing — chính là thứ làm Go interfaces cực kỳ linh hoạt.

Các Pattern Interface Phổ Biến

Interface Nhỏ Là Tốt Nhất

Standard library của Go ưu tiên interface nhỏ gọn:

// Một method — cực kỳ hữu dụng
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
}
 
// Compose interface nhỏ thành lớn hơn
type ReadWriter interface {
    Reader
    Writer
}
 
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

"Interface càng lớn, abstraction càng yếu." — Rob Pike. Giữ interface nhỏ (1-3 methods). Compose khi cần.

Interface Làm Parameter

Nhận interface, trả concrete type:

type Storage interface {
    Save(key string, data []byte) error
    Load(key string) ([]byte, error)
}
 
// MemoryStorage implement Storage
type MemoryStorage struct {
    data map[string][]byte
}
 
func NewMemoryStorage() *MemoryStorage {
    return &MemoryStorage{data: make(map[string][]byte)}
}
 
func (m *MemoryStorage) Save(key string, data []byte) error {
    m.data[key] = data
    return nil
}
 
func (m *MemoryStorage) Load(key string) ([]byte, error) {
    data, ok := m.data[key]
    if !ok {
        return nil, fmt.Errorf("key not found: %s", key)
    }
    return data, nil
}
 
// Nhận interface — dễ test và linh hoạt
func ProcessData(store Storage, key string) error {
    data := []byte("processed data")
    return store.Save(key, data)
}

Empty Interface và any

Empty interface interface{} (hoặc alias any từ Go 1.18) nhận bất kỳ type nào:

func PrintAnything(v any) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}
 
PrintAnything(42)          // Type: int, Value: 42
PrintAnything("hello")     // Type: string, Value: hello
PrintAnything(true)        // Type: bool, Value: true

Dùng any hạn chế. Nó bỏ qua type safety. Ưu tiên interface cụ thể hoặc generics (Go 1.18+) khi có thể.

Type Assertions và Type Switches

Khi nhận interface value, bạn có thể kiểm tra underlying type:

func Describe(i any) string {
    // Type switch — check nhiều types
    switch v := i.(type) {
    case string:
        return fmt.Sprintf("string dài %d", len(v))
    case int:
        return fmt.Sprintf("integer: %d", v)
    case bool:
        return fmt.Sprintf("boolean: %t", v)
    default:
        return fmt.Sprintf("type không xác định: %T", v)
    }
}
 
// Type assertion — lấy type cụ thể
func MustBeString(i any) string {
    s, ok := i.(string) // safe assertion — không panic
    if !ok {
        return "not a string"
    }
    return s
}

Gotcha: Interface Nil

Một trong những hành vi khó hiểu nhất của Go — interface value chỉ nil khi cả type và value đều nil:

type MyError struct {
    msg string
}
 
func (e *MyError) Error() string { return e.msg }
 
func DoSomething() error {
    var err *MyError = nil // typed nil pointer
    return err             // ⚠️ KHÔNG nil! Interface có type (*MyError) với value nil
}
 
func main() {
    err := DoSomething()
    if err != nil {
        // Code này chạy! Dù underlying pointer là nil
        fmt.Println("Error:", err) // panic: nil pointer dereference
    }
}
 
// ✅ Sửa: return nil trực tiếp
func DoSomethingFixed() error {
    var err *MyError = nil
    if err != nil {
        return err
    }
    return nil // Untyped nil — interface thực sự nil
}

Phần 3: Error Handling

Go không có exceptions. Thay vào đó, errors là values — functions trả về chúng, callers kiểm tra chúng. Đây là thiết kế có chủ đích: buộc bạn phải nghĩ về mọi điểm có thể thất bại.

error Interface

error interface tích hợp chỉ là:

type error interface {
    Error() string
}

Bất kỳ type nào có method Error() string đều là error.

Error Handling Cơ Bản

import (
    "errors"
    "fmt"
    "os"
)
 
// Functions trả error là giá trị cuối cùng
func ReadConfig(path string) (string, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return "", err // truyền error lên
    }
    return string(data), nil
}
 
func main() {
    config, err := ReadConfig("/etc/app.conf")
    if err != nil {
        fmt.Println("Không đọc được config:", err)
        return
    }
    fmt.Println("Config:", config)
}

Pattern if err != nil là đặc trưng của Go. Bạn sẽ viết nó hàng trăm lần. Dài dòng nhưng rõ ràng — luôn biết chính xác error được xử lý ở đâu.

Tạo Errors

import (
    "errors"
    "fmt"
)
 
// Error đơn giản với errors.New
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}
 
// Error có format với fmt.Errorf
func ValidateAge(age int) error {
    if age < 0 {
        return fmt.Errorf("tuổi không hợp lệ: %d (phải >= 0)", age)
    }
    if age > 150 {
        return fmt.Errorf("tuổi không hợp lệ: %d (phải <= 150)", age)
    }
    return nil
}

Custom Error Types

Khi error cần mang dữ liệu có cấu trúc:

// 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)
}
 
type NotFoundError struct {
    Resource string
    ID       string
}
 
func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s with ID %s not found", e.Resource, e.ID)
}
 
// Trả custom errors
func FindUser(id string) (*User, error) {
    // ... tra database
    return nil, &NotFoundError{Resource: "User", ID: id}
}
 
// Kiểm tra error type cụ thể
func HandleRequest(id string) {
    user, err := FindUser(id)
    if err != nil {
        var notFound *NotFoundError
        if errors.As(err, &notFound) {
            fmt.Printf("404: %s %s not found\n", notFound.Resource, notFound.ID)
            return
        }
        fmt.Println("500: Internal error:", err)
        return
    }
    fmt.Println("Found user:", user)
}

Error Wrapping (Go 1.13+)

Wrap error kèm context bằng fmt.Errorf và %w:

func ReadUserConfig(userID string) (Config, error) {
    path := fmt.Sprintf("/home/%s/.config", userID)
 
    data, err := os.ReadFile(path)
    if err != nil {
        // Wrap kèm context — error gốc được giữ nguyên
        return Config{}, fmt.Errorf("reading config for user %s: %w", userID, err)
    }
 
    config, err := parseConfig(data)
    if err != nil {
        return Config{}, fmt.Errorf("parsing config for user %s: %w", userID, err)
    }
 
    return config, nil
}
 
// Unwrap để kiểm tra error gốc
func main() {
    config, err := ReadUserConfig("alice")
    if err != nil {
        // errors.Is kiểm tra toàn bộ error chain
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("File config không tồn tại — dùng giá trị mặc định")
            return
        }
        // errors.As lấy type cụ thể từ chain
        var pathErr *os.PathError
        if errors.As(err, &pathErr) {
            fmt.Printf("Path error on %s: %v\n", pathErr.Path, pathErr.Err)
            return
        }
        fmt.Println("Unexpected error:", err)
    }
}

Sentinel Errors

Errors định nghĩa sẵn cho các điều kiện phổ biến:

import "errors"
 
// Định nghĩa sentinel errors ở package level
var (
    ErrNotFound     = errors.New("not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrConflict     = errors.New("conflict")
)
 
func GetUser(id string) (*User, error) {
    // ... lookup
    return nil, fmt.Errorf("user %s: %w", id, ErrNotFound)
}
 
func main() {
    _, err := GetUser("123")
    if errors.Is(err, ErrNotFound) {
        fmt.Println("User not found") // hoạt động kể cả khi đã wrapped
    }
}

Error Handling Tóm Tắt

FunctionMục ĐíchVí Dụ
errors.New(msg)Tạo error đơn giảnerrors.New("something failed")
fmt.Errorf("...: %w", err)Wrap error kèm contextfmt.Errorf("reading file: %w", err)
errors.Is(err, target)Kiểm tra error có match không (kiểm tra cả chain)errors.Is(err, os.ErrNotExist)
errors.As(err, &target)Lấy typed error (kiểm tra cả chain)errors.As(err, &pathErr)

Phần 4: Packages và Visibility

Packages là cách Go tổ chức code. Mỗi file Go thuộc về một package, và hệ thống package kiểm soát thứ gì được nhìn thấy từ bên ngoài.

Package Cơ Bản

myproject/
├── go.mod
├── main.go            // package main
├── user/
│   ├── user.go        // package user
│   └── validation.go  // package user
└── store/
    └── memory.go      // package store
// user/user.go
package user
 
// Exported (chữ hoa) — nhìn thấy từ ngoài package
type User struct {
    ID    int
    Name  string
    email string // unexported (chữ thường) — chỉ thấy trong package user
}
 
// Exported function
func New(id int, name, email string) User {
    return User{ID: id, Name: name, email: email}
}
 
// Exported method
func (u User) DisplayName() string {
    return u.Name
}
 
// unexported method — chỉ gọi được trong package user
func (u User) emailDomain() string {
    // ...
    return ""
}
// main.go
package main
 
import (
    "fmt"
    "myproject/user"
)
 
func main() {
    u := user.New(1, "Alice", "[email protected]")
    fmt.Println(u.Name)          // ✅ exported field
    fmt.Println(u.DisplayName()) // ✅ exported method
    // fmt.Println(u.email)      // ❌ compile error — unexported field
    // fmt.Println(u.emailDomain()) // ❌ compile error — unexported method
}

Quy Tắc Visibility

Tên bắt đầu bằngVisibilityVí Dụ
Chữ hoaExported (public)User, New(), ID
Chữ thườngUnexported (package-private)email, emailDomain()

Không có keyword public/private. Chỉ viết hoa hay viết thường. Đơn giản và compiler enforce.

Quy Ước Đặt Tên Package

// ✅ Tên package tốt — ngắn, chữ thường, một từ
package user
package store
package http
package json
 
// ❌ Tên package tệ
package userManager    // đừng dùng camelCase
package user_service   // đừng dùng underscores
package util           // quá chung chung — nó làm gì?
package helpers        // quá chung chung
package common         // quá chung chung

Tránh package utils, helpers, common. Chúng sẽ thành nơi đổ rác. Đặt tên theo thứ package cung cấp, không phải cách nó được dùng.

Hàm init()

Mỗi package có thể có hàm init() chạy tự động khi khởi động:

package database
 
import "fmt"
 
var connectionPool *Pool
 
func init() {
    // Chạy tự động khi package được import
    connectionPool = NewPool(10)
    fmt.Println("Database pool initialized")
}

Dùng init() hạn chế — nó chạy trước main() và khiến startup khó đoán. Ưu tiên khởi tạo tường minh.


Phần 5: Go Modules

Go modules là cách chuẩn để quản lý dependencies và versioning.

Tạo Module

# Tạo module mới
mkdir myproject && cd myproject
go mod init github.com/username/myproject
 
# Tạo ra file go.mod:
# module github.com/username/myproject
# go 1.22

File go.mod

module github.com/username/myproject
 
go 1.22
 
require (
    github.com/gin-gonic/gin v1.9.1
    github.com/lib/pq v1.10.9
)
 
require (
    // indirect dependencies (tự quản lý)
    golang.org/x/net v0.17.0 // indirect
)

Các Lệnh Module Thiết Yếu

# Thêm dependency
go get github.com/gin-gonic/gin
 
# Thêm version cụ thể
go get github.com/lib/[email protected]
 
# Dọn dẹp — xóa dependencies không dùng, thêm thiếu
go mod tidy
 
# Tải tất cả dependencies về cache local
go mod download
 
# Liệt kê tất cả dependencies
go list -m all
 
# Kiểm tra bản cập nhật
go list -m -u all
 
# Cập nhật dependency
go get -u github.com/gin-gonic/gin

File go.sum

go.sum chứa hash mã hóa của dependencies. Luôn commit nó — đảm bảo build nhất quán:

github.com/gin-gonic/gin v1.9.1 h1:4+fr/el88...
github.com/gin-gonic/gin v1.9.1/go.mod h1:2fq3...

Best Practices Module

Thực HànhLý Do
Chạy go mod tidy trước khi commitXóa deps thừa, thêm deps thiếu
Commit cả go.mod lẫn go.sumĐảm bảo build nhất quán
Dùng version cụ thểTránh breaking changes từ @latest
Cập nhật dependencies định kỳSecurity patches và bug fixes

Phần 6: Standard Library Thiết Yếu

Go có standard library phong phú. Đây là những package bạn dùng nhiều nhất:

fmt — Formatted I/O

name := "Alice"
age := 30
 
// Print
fmt.Println("Hello, World!")                    // có newline
fmt.Printf("Name: %s, Age: %d\n", name, age)   // formatted
fmt.Print("No newline")                          // không newline
 
// Sprint — format thành string
msg := fmt.Sprintf("Hello, %s! You are %d.", name, age)
 
// Format verbs phổ biến
fmt.Printf("%v\n", user)     // format mặc định
fmt.Printf("%+v\n", user)    // kèm tên field: {ID:1 Name:Alice}
fmt.Printf("%#v\n", user)    // Go-syntax: main.User{ID:1, Name:"Alice"}
fmt.Printf("%T\n", user)     // type: main.User
fmt.Printf("%d\n", 42)       // integer
fmt.Printf("%f\n", 3.14)     // float
fmt.Printf("%s\n", "hello")  // string
fmt.Printf("%q\n", "hello")  // quoted string: "hello"
fmt.Printf("%x\n", 255)      // hex: ff
fmt.Printf("%p\n", &user)    // địa chỉ pointer

strings — Xử Lý Chuỗi

import "strings"
 
s := "Hello, World!"
 
strings.Contains(s, "World")           // true
strings.HasPrefix(s, "Hello")          // true
strings.HasSuffix(s, "!")              // true
strings.ToUpper(s)                     // "HELLO, WORLD!"
strings.ToLower(s)                     // "hello, world!"
strings.TrimSpace("  hello  ")         // "hello"
strings.Split("a,b,c", ",")           // ["a", "b", "c"]
strings.Join([]string{"a", "b"}, "-")  // "a-b"
strings.Replace(s, "World", "Go", 1)   // "Hello, Go!"
strings.Count(s, "l")                  // 3
strings.Index(s, "World")              // 7

strconv — Chuyển Đổi Chuỗi

import "strconv"
 
// String sang number
i, err := strconv.Atoi("42")            // int: 42
f, err := strconv.ParseFloat("3.14", 64) // float64: 3.14
b, err := strconv.ParseBool("true")      // bool: true
 
// Number sang string
s := strconv.Itoa(42)                    // "42"
s = strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
s = strconv.FormatBool(true)              // "true"

os — Tương Tác Hệ Điều Hành

import "os"
 
// Biến môi trường
home := os.Getenv("HOME")
port := os.Getenv("PORT")
 
// Thao tác file
data, err := os.ReadFile("config.json")  // đọc toàn bộ file
err = os.WriteFile("output.txt", []byte("hello"), 0644) // ghi file
 
// Mở file cho thao tác chi tiết hơn
file, err := os.Open("data.txt")         // chỉ đọc
file, err = os.Create("new.txt")          // tạo/truncate
defer file.Close()
 
// Command-line arguments
args := os.Args    // os.Args[0] là tên chương trình
if len(args) > 1 {
    fmt.Println("First arg:", args[1])
}
 
// Thoát
os.Exit(1) // thoát với status code

io — I/O Primitives

import (
    "io"
    "os"
    "strings"
)
 
// io.Reader và io.Writer là các abstraction cốt lõi
// Nhiều types implement chúng: files, network connections, buffers...
 
// Copy từ reader sang writer
src := strings.NewReader("Hello, World!")
dst := os.Stdout
io.Copy(dst, src) // prints "Hello, World!"
 
// Đọc tất cả bytes từ reader
data, err := io.ReadAll(strings.NewReader("data"))

Best Practices Tóm Tắt

Structs & Methods

Thực HànhVí Dụ
Dùng pointer receiver nếu bất kỳ method nào mutateTất cả method trên type nên dùng *T
Cung cấp constructor functionfunc NewUser(name string) *User
Giữ struct tập trungMỗi struct một trách nhiệm
Embed cho composition, không phải inheritancetype Admin struct { User }

Interfaces

Thực HànhVí Dụ
Giữ interface nhỏ (1-3 methods)type Reader interface { Read(p []byte) (int, error) }
Định nghĩa interface ở nơi dùng, không phải nơi implementPackage consumer định nghĩa interface
Nhận interface, trả concrete typefunc NewServer(store Storage) *Server
Đừng tạo interface quá sớmĐợi khi có 2+ implementations

Error Handling

Thực HànhVí Dụ
Luôn kiểm tra error trả vềif err != nil { return err }
Thêm context khi wrappingfmt.Errorf("reading user %s: %w", id, err)
Dùng sentinel errors cho điều kiện expectedvar ErrNotFound = errors.New("not found")
Dùng custom types cho structured errorstype ValidationError struct { ... }

Packages

Thực HànhVí Dụ
Tên ngắn, chữ thường, một từuser, store, auth
Tránh package utils/helpersĐặt tên theo thứ nó cung cấp
Giữ init() đơn giản hoặc tránh dùngƯu tiên khởi tạo tường minh
Chạy go mod tidy trước khi commitDọn dẹp dependencies thừa

Sai Lầm Thường Gặp

1. Trả Typed Nil (Interface Gotcha)

// SAI: Trả interface non-nil với value nil
func GetError() error {
    var err *MyError = nil
    return err // interface{type: *MyError, value: nil} — KHÔNG nil!
}
 
// ĐÚNG: Trả untyped nil
func GetError() error {
    var err *MyError = nil
    if err != nil {
        return err
    }
    return nil // interface thực sự nil
}

2. Bỏ Qua Errors

// SAI: Im lặng bỏ qua error
data, _ := os.ReadFile("config.json") // Nếu file không tồn tại thì sao?
 
// ĐÚNG: Xử lý hoặc ít nhất là log
data, err := os.ReadFile("config.json")
if err != nil {
    log.Fatal("failed to read config:", err)
}

3. Interface Quá Lớn

// SAI: Interface "bếp chén" — khó implement, khó test
type UserService interface {
    Create(u User) error
    Update(u User) error
    Delete(id string) error
    Find(id string) (*User, error)
    List() ([]User, error)
    SendEmail(id, subject, body string) error
    ResetPassword(id string) error
    GenerateReport() ([]byte, error)
}
 
// ĐÚNG: Interface nhỏ, tập trung
type UserReader interface {
    Find(id string) (*User, error)
    List() ([]User, error)
}
 
type UserWriter interface {
    Create(u User) error
    Update(u User) error
    Delete(id string) error
}

4. Không Dùng errors.Is / errors.As

// SAI: So sánh trực tiếp — hỏng khi error bị wrapped
if err == os.ErrNotExist {
    // Không match nếu err đã bị wrap bằng fmt.Errorf("%w", ...)
}
 
// ĐÚNG: errors.Is kiểm tra toàn bộ error chain
if errors.Is(err, os.ErrNotExist) {
    // Hoạt động kể cả với wrapped errors
}

Bài Tập Thực Hành

Bài 1: Shape Calculator

Xây shape calculator dùng interfaces:

type Shape interface {
    Area() float64
    Perimeter() float64
    String() string
}

Implement Circle, Rectangle và Triangle. Viết hàm PrintShapeInfo(s Shape) hoạt động với mọi shape.

Lời giải:

package main
 
import (
    "fmt"
    "math"
)
 
type Shape interface {
    Area() float64
    Perimeter() float64
    String() string
}
 
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 }
func (c Circle) String() string     { return fmt.Sprintf("Circle(r=%.1f)", c.Radius) }
 
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) }
func (r Rectangle) String() string {
    return fmt.Sprintf("Rectangle(%.1f x %.1f)", r.Width, r.Height)
}
 
type Triangle struct {
    A, B, C float64 // các cạnh
}
 
func (t Triangle) Area() float64 {
    s := t.Perimeter() / 2 // nửa chu vi (công thức Heron)
    return math.Sqrt(s * (s - t.A) * (s - t.B) * (s - t.C))
}
 
func (t Triangle) Perimeter() float64 { return t.A + t.B + t.C }
func (t Triangle) String() string {
    return fmt.Sprintf("Triangle(%.1f, %.1f, %.1f)", t.A, t.B, t.C)
}
 
func PrintShapeInfo(s Shape) {
    fmt.Printf("%s → Area: %.2f, Perimeter: %.2f\n", s, s.Area(), s.Perimeter())
}
 
func main() {
    shapes := []Shape{
        Circle{Radius: 5},
        Rectangle{Width: 4, Height: 6},
        Triangle{A: 3, B: 4, C: 5},
    }
 
    for _, s := range shapes {
        PrintShapeInfo(s)
    }
}
// Circle(r=5.0) → Area: 78.54, Perimeter: 31.42
// Rectangle(4.0 x 6.0) → Area: 24.00, Perimeter: 20.00
// Triangle(3.0, 4.0, 5.0) → Area: 6.00, Perimeter: 12.00

Bài 2: Error Handling Chain

Xây hàm đăng ký user với validation và error handling đúng cách:

// Implement:
// - ValidateEmail(email string) error
// - ValidatePassword(password string) error
// - RegisterUser(name, email, password string) (*User, error)
//
// Dùng custom error types, error wrapping và errors.As để xử lý.

Lời giải:

package main
 
import (
    "errors"
    "fmt"
    "strings"
    "unicode"
)
 
type ValidationError struct {
    Field   string
    Message string
}
 
func (e *ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
 
var ErrUserExists = errors.New("user already exists")
 
func ValidateEmail(email string) error {
    if !strings.Contains(email, "@") {
        return &ValidationError{Field: "email", Message: "phải chứa @"}
    }
    if !strings.Contains(email, ".") {
        return &ValidationError{Field: "email", Message: "phải chứa domain"}
    }
    return nil
}
 
func ValidatePassword(password string) error {
    if len(password) < 8 {
        return &ValidationError{Field: "password", Message: "phải ít nhất 8 ký tự"}
    }
    hasUpper := false
    hasDigit := false
    for _, r := range password {
        if unicode.IsUpper(r) { hasUpper = true }
        if unicode.IsDigit(r) { hasDigit = true }
    }
    if !hasUpper {
        return &ValidationError{Field: "password", Message: "phải chứa chữ hoa"}
    }
    if !hasDigit {
        return &ValidationError{Field: "password", Message: "phải chứa số"}
    }
    return nil
}
 
type User struct {
    Name  string
    Email string
}
 
var existingUsers = map[string]bool{"[email protected]": true}
 
func RegisterUser(name, email, password string) (*User, error) {
    if err := ValidateEmail(email); err != nil {
        return nil, fmt.Errorf("đăng ký thất bại: %w", err)
    }
    if err := ValidatePassword(password); err != nil {
        return nil, fmt.Errorf("đăng ký thất bại: %w", err)
    }
    if existingUsers[email] {
        return nil, fmt.Errorf("đăng ký cho %s: %w", email, ErrUserExists)
    }
 
    user := &User{Name: name, Email: email}
    existingUsers[email] = true
    return user, nil
}
 
func main() {
    testCases := []struct{ name, email, password string }{
        {"Alice", "alice", "Pass1234"},               // email sai
        {"Bob", "[email protected]", "short"},              // password yếu
        {"Carol", "[email protected]", "Pass1234"},    // trùng user
        {"Dave", "[email protected]", "Secure1234"},       // thành công
    }
 
    for _, tc := range testCases {
        user, err := RegisterUser(tc.name, tc.email, tc.password)
        if err != nil {
            var valErr *ValidationError
            if errors.As(err, &valErr) {
                fmt.Printf("Validation error on %s: %s\n", valErr.Field, valErr.Message)
            } else if errors.Is(err, ErrUserExists) {
                fmt.Printf("User %s đã tồn tại\n", tc.email)
            } else {
                fmt.Println("Unexpected error:", err)
            }
            continue
        }
        fmt.Printf("Đăng ký thành công: %s (%s)\n", user.Name, user.Email)
    }
}

Bài 3: Simple Key-Value Store

Xây key-value store với Storage interface, MemoryStore implementation, và LoggingStore decorator log mọi thao tác:

type Storage interface {
    Get(key string) (string, error)
    Set(key string, value string) error
    Delete(key string) error
}

Implement MemoryStore và LoggingStore (bọc bất kỳ Storage nào). Viết main() demo cả hai.

Lời giải:

package main
 
import (
    "errors"
    "fmt"
    "time"
)
 
var ErrKeyNotFound = errors.New("key not found")
 
type Storage interface {
    Get(key string) (string, error)
    Set(key, value string) error
    Delete(key string) error
}
 
// MemoryStore — implementation in-memory
type MemoryStore struct {
    data map[string]string
}
 
func NewMemoryStore() *MemoryStore {
    return &MemoryStore{data: make(map[string]string)}
}
 
func (m *MemoryStore) Get(key string) (string, error) {
    val, ok := m.data[key]
    if !ok {
        return "", fmt.Errorf("get %q: %w", key, ErrKeyNotFound)
    }
    return val, nil
}
 
func (m *MemoryStore) Set(key, value string) error {
    m.data[key] = value
    return nil
}
 
func (m *MemoryStore) Delete(key string) error {
    if _, ok := m.data[key]; !ok {
        return fmt.Errorf("delete %q: %w", key, ErrKeyNotFound)
    }
    delete(m.data, key)
    return nil
}
 
// LoggingStore — decorator log mọi thao tác
type LoggingStore struct {
    inner Storage
}
 
func NewLoggingStore(inner Storage) *LoggingStore {
    return &LoggingStore{inner: inner}
}
 
func (l *LoggingStore) Get(key string) (string, error) {
    start := time.Now()
    val, err := l.inner.Get(key)
    fmt.Printf("[GET] key=%q duration=%v err=%v\n", key, time.Since(start), err)
    return val, err
}
 
func (l *LoggingStore) Set(key, value string) error {
    start := time.Now()
    err := l.inner.Set(key, value)
    fmt.Printf("[SET] key=%q value=%q duration=%v err=%v\n", key, value, time.Since(start), err)
    return err
}
 
func (l *LoggingStore) Delete(key string) error {
    start := time.Now()
    err := l.inner.Delete(key)
    fmt.Printf("[DEL] key=%q duration=%v err=%v\n", key, time.Since(start), err)
    return err
}
 
func main() {
    var store Storage = NewLoggingStore(NewMemoryStore())
 
    store.Set("name", "Alice")
    store.Set("role", "admin")
 
    val, _ := store.Get("name")
    fmt.Println("Got:", val)
 
    store.Delete("role")
 
    _, err := store.Get("role")
    if errors.Is(err, ErrKeyNotFound) {
        fmt.Println("Key đã xóa thành công")
    }
}

Tiếp Theo

Bạn đã hiểu hệ thống type, interfaces, error handling và package organization — nền tảng cốt lõi của mọi chương trình Go.

Trong Phase 3: Concurrency & Advanced Patterns, bạn sẽ học:

  • Goroutines và Go scheduler
  • Channels cho giao tiếp giữa các goroutines
  • Select statements và patterns
  • sync package (Mutex, WaitGroup, Once)
  • Context cho cancellation và timeouts

Bài viết liên quan:


Tóm Tắt

Trong bài này, bạn đã học:

✅ Structs & Methods: Định nghĩa custom types, gắn behavior với value và pointer receivers
✅ Embedding: Compose types bằng struct embedding thay vì inheritance
✅ Interfaces: Implicit implementation, interface nhỏ, type assertions và switches
✅ Error Handling: error interface, custom errors, wrapping với %w, errors.Is/errors.As
✅ Packages: Exported vs unexported, quy ước đặt tên, hàm init()
✅ Go Modules: go.mod, go.sum, các lệnh thiết yếu (go get, go mod tidy)
✅ Standard Library: fmt, strings, strconv, os, io cơ bản

Bạn đã sẵn sàng chinh phục tính năng đặc trưng nhất của Go — concurrency với goroutines và channels ở Phase 3!


Series: Go Learning Roadmap
Previous: Phase 1: Go Fundamentals
Next: Phase 3: Concurrency & Advanced Patterns (coming soon)

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.