Golang cơ bản- Học trong 7 ngày - Part 2 🚀

EminelEminel
10 min read

📚 Ngày 5: Goroutine, Channel, Make

1. Giới thiệu về Goroutine

Goroutine là một trong những tính năng nổi bật nhất của ngôn ngữ Go, được thiết kế để hỗ trợ lập trình đồng thời (concurrent programming) một cách đơn giản và hiệu quả. Goroutine có thể được coi là một hàm hoặc phương thức chạy độc lập với chương trình chính, cho phép thực hiện nhiều tác vụ song song.

2. Goroutine vs Thread truyền thống

Kích thước và tài nguyên

  • Thread: Thường có kích thước stack cố định (thường là 1-2MB), tốn nhiều tài nguyên hệ thống

  • Goroutine: Bắt đầu với stack nhỏ (chỉ khoảng 2KB) và có thể mở rộng/thu hẹp động theo nhu cầu

Số lượng

  • Thread: Hệ thống thường chỉ hỗ trợ hàng nghìn thread

  • Goroutine: Có thể chạy hàng trăm nghìn, thậm chí hàng triệu goroutine trên một máy thông thường

Quản lý

  • Thread: Quản lý bởi hệ điều hành

  • Goroutine: Quản lý bởi Go runtime (scheduler của Go), độc lập với OS thread

Chuyển đổi ngữ cảnh (Context switching)

  • Thread: Tốn nhiều chi phí do phải lưu trữ và khôi phục nhiều thông tin

  • Goroutine: Chi phí thấp hơn nhiều vì được quản lý trong không gian người dùng

3. Go Runtime Scheduler

Scheduler của Go phân phối goroutine trên một tập các OS thread:

  • Sử dụng M:N scheduling model

  • M goroutines chạy trên N OS threads

  • Mặc định N = GOMAXPROCS (thường bằng số lõi CPU)

  • Tự động điều phối goroutine giữa các thread

4. Cú pháp và cách sử dụng Goroutine, Channel

Cú pháp cơ bản Goroutine

go functionName(parameters)

Ví dụ đơn giản

func sayHello() {
    fmt.Println("Hello from goroutine!")
}

func main() {
    go sayHello() // Chạy sayHello() trong một goroutine mới

    // Cần một cơ chế để đợi goroutine hoàn thành
    // Nếu không, chương trình chính có thể kết thúc trước
    time.Sleep(100 * time.Millisecond)
}

Sử dụng với hàm ẩn danh (anonymous function)

func main() {
    go func() {
        fmt.Println("Hello from anonymous goroutine!")
    }()

    time.Sleep(100 * time.Millisecond)
}

Cú pháp cơ bản Channel

ch := make(chan string) // Tạo một kênh kiểu string
ch <- 1 // gửi giá trị
value := <-ch // nhận giá trị

Ví dụ đơn giản

package main

import "fmt"

// hàm sayHi sẽ gửi một chuỗi vào kênh

func sayHi(ch chan string) {
    ch <- "Hello from Goroutine!"
}

func main2() {
    ch := make(chan string) // Tạo một kênh kiểu string
    go sayHi(ch)            // Gọi hàm sayHi trong một goroutine mới
    msg := <-ch             // Nhận thông điệp từ kênh
    fmt.Println(msg)
}

5. WaitGroup để đồng bộ hóa Goroutine

Trong thực tế, thay vì sử dụng time.Sleep(), chúng ta thường dùng sync.WaitGroup để đợi các goroutine hoàn thành:

func main() {
    var wg sync.WaitGroup

    // Thêm 1 goroutine vào WaitGroup
    wg.Add(1)

    go func() {
        // Báo hiệu goroutine đã hoàn thành khi thoát
        defer wg.Done()

        fmt.Println("Working in goroutine")
    }()

    // Đợi tất cả goroutine trong WaitGroup hoàn thành
    wg.Wait()
    fmt.Println("Main goroutine exits")
}

6. Chia sẻ dữ liệu giữa các Goroutine

Lưu ý về race condition

Khi nhiều goroutine cùng truy cập vào một biến, có thể xảy ra race condition:

func main() {
    counter := 0
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++ // Race condition
        }()
    }

    wg.Wait()
    fmt.Println("Counter:", counter) // Kết quả không chính xác
}

Giải pháp với Mutex

func main() {
    counter := 0
    var wg sync.WaitGroup
    var mu sync.Mutex

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            mu.Lock()
            counter++
            mu.Unlock()
        }()
    }

    wg.Wait()
    fmt.Println("Counter:", counter) // Kết quả chính xác
}

7. Goroutine Leaks và cách tránh

Goroutine leak xảy ra khi goroutine không bao giờ kết thúc, dẫn đến rò rỉ bộ nhớ:

// Goroutine leak
func leakyFunction() {
    ch := make(chan int)
    go func() {
        val := <-ch // Goroutine này sẽ chờ mãi mãi
        fmt.Println("Received:", val)
    }()
    // Không bao giờ gửi giá trị vào channel
    // Goroutine sẽ không bao giờ kết thúc
}

Các cách tránh leak:

  • Luôn có cơ chế đóng channel

  • Sử dụng context để hủy goroutine

  • Đảm bảo tất cả goroutine đều có cơ chế thoát

8. Những điểm cần lưu ý

  • Goroutine không có ID hay cách truy cập trực tiếp từ bên ngoài

  • Main goroutine kết thúc sẽ dừng tất cả goroutine khác

  • Nên sử dụng channel để giao tiếp giữa các goroutine

  • Tránh sử dụng biến được chia sẻ nếu có thể

  • Sử dụng go run -race để phát hiện race condition

  • Goroutine rất nhẹ nhưng không phải miễn phí, cần tránh lạm dụng quá nhiều

9. Các pattern phổ biến

Worker Pool

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, j)
        results <- j * 2
    }
}

func main() {
    numJobs := 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // Khởi động 3 worker
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Gửi công việc
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // Thu thập kết quả
    for a := 1; a <= numJobs; a++ {
        <-results
    }
}

Pipeline

func generator(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {
    // Tạo pipeline: generator -> square -> in kết quả
    for n := range square(generator(1, 2, 3, 4, 5)) {
        fmt.Println(n)
    }
}

Goroutine là một trong những tính năng mạnh mẽ nhất của Go, cho phép xây dựng các chương trình đồng thời một cách dễ dàng và hiệu quả. Hiểu rõ về cách hoạt động và cách sử dụng goroutine sẽ giúp bạn khai thác tối đa sức mạnh của Go trong các ứng dụng cần xử lý đồng thời.

10. Make là gì?

makehàm built-in của Go, được dùng để khởi tạo:

  • Slice

  • Map

  • Channel

❌ Không dùng cho array hoặc struct nhé.
✅ Chỉ dùng cho kiểu “reference types” mà cần được khởi tạo trước khi dùng.

📌 Cách dùng make

1. Khởi tạo slice:

s := make([]int, 5) // slice có 5 phần tử, giá trị mặc định là 0
s := make([]int, 2, 5) 
// len = 2 (số phần tử đang dùng)
// cap = 5 (dung lượng tối đa trước khi phải cấp phát lại)

👉 Khi bạn cần một slice có sẵn dung lượng để tránh cấp phát lại khi append, hãy dùng make.


2. Khởi tạo map:

m := make(map[string]int)
m["apple"] = 10

👉 Nếu bạn khai báo map bằng var m map[string]int, thì phải dùng make trước khi gán:

var m map[string]int
m["apple"] = 10 // ❌ panic: assignment to entry in nil map

3. Khởi tạo channel:

ch := make(chan int, 3) // channel có buffer 3 phần tử

👉 Nếu bạn muốn gửi/nhận dữ liệu giữa các goroutine, make sẽ khởi tạo channel để sử dụng ngay.

🤔 Vậy new khác gì make?

newmake
Trả về con trỏ tới vùng nhớTrả về giá trị đã khởi tạo sẵn
Dùng cho bất kỳ kiểu nàoChỉ dùng cho: slice, map, channel
Giá trị khởi tạo là zeroGiá trị usable ngay lập tức
p := new([]int)   // p là con trỏ tới slice, nhưng chưa usable
*s = append(*p, 1) // phải giải deref trước

s := make([]int, 0) // slice usable ngay

✅ Khi nào nên dùng make?

  • Khi bạn cần slice với độ dài/capacity xác định từ đầu (tối ưu performance).

  • Khi bạn cần tạo map hoặc channel trước khi dùng.

  • Khi bạn cần tạo một slice/map/channel non-nil để tránh runtime error.

📘 Ngày 6: Module và Tổ chức Project trong Go

🎯 Mục tiêu

  • Hiểu go mod là gì và cách sử dụng trong Go.

  • Biết cách tổ chức một project theo module và package.

  • Biết chia nhỏ chương trình ra nhiều file và package để dễ quản lý, mở rộng và bảo trì.


📦 1. Go Module là gì?

go mod là hệ thống quản lý dependency chính thức của Go (từ Go 1.11).

✨ Lợi ích:

  • Quản lý thư viện bên ngoài rõ ràng.

  • Tự động tải và lưu version dependency.

  • Cho phép chia sẻ project dễ dàng hơn.

🔧 Một số lệnh thường dùng

LệnhMô tả
go mod init <module>Tạo file go.mod và khởi tạo module
go mod tidyDọn dẹp và tải các package cần thiết
go get <package>Cài package bên ngoài vào project

🧱 2. Tổ chức Project theo Module và Package

📁 Ví dụ cấu trúc project:

userapp/
├── go.mod
├── main.go
├── models/
│   └── user.go
├── services/
│   └── user_service.go
└── utils/
    └── string_utils.go

📌 Nguyên tắc tổ chức:

  • Mỗi thư mục là một package.

  • Một package có thể chứa nhiều file .go (cùng tên package).

  • Không nên import vòng (import lẫn nhau).

  • Tách rõ phần model, logic nghiệp vụ, và hàm tiện ích.


📂 3. Chia Code ra Nhiều File

Bạn có thể chia nhỏ code trong cùng một package ra nhiều file khác nhau.

Ví dụ:

// models/user.go
package models

type User struct {
    ID   int
    Name string
    Age  int
}
// services/user_service.go
package services

import (
    "fmt"
    "userapp/models"
)

func PrintUserInfo(u models.User) {
    fmt.Printf("ID: %d, Name: %s, Age: %d\n", u.ID, u.Name, u.Age)
}

📘Ngày 7 - Xây dựng mini project

Cấu trúc Dự án

// PROJECT STRUCTURE
// ----------------
// go-bookstore/
// ├── cmd/
// │   └── api/
// │       └── main.go
// ├── internal/
// │   ├── controller/
// │   │   └── book_controller.go
// │   ├── model/
// │   │   └── book.go
// │   ├── repository/
// │   │   └── book_repository.go
// │   └── service/
// │       └── book_service.go
// ├── pkg/
// │   └── response/
// │       └── response.go
// └── go.mod

Dự án được tổ chức theo mô hình sạch với cấu trúc thư mục rõ ràng:

  • cmd/api/main.go: Điểm khởi đầu của ứng dụng

  • internal/: Chứa code cốt lõi của ứng dụng

    • controller/: Xử lý HTTP request/response

    • model/: Định nghĩa cấu trúc dữ liệu

    • repository/: Xử lý lưu trữ dữ liệu

    • service/: Logic nghiệp vụ

  • pkg/: Mã có thể tái sử dụng cho các dự án khác

Tính Năng Chính

  • CRUD operations cho sách:

    • Tạo sách mới

    • Lấy tất cả sách

    • Lấy sách theo ID

    • Cập nhật thông tin sách

    • Xóa sách

  • In-memory Repository:

    • Lưu trữ dữ liệu trong bộ nhớ (dễ thay thế bằng database thật)

    • Thread-safe với mutex

  • Chuẩn hóa response:

    • Format phản hồi nhất quán

    • Xử lý lỗi chuyên nghiệp

Cách Chạy Dự Án

  1. Tạo cấu trúc thư mục như đã mô tả ở trên

  2. Khởi tạo module: go mod init go-bookstore

  3. Cài đặt dependencies: go get github.com/gin-gonic/gin github.com/google/uuid

  4. Chạy ứng dụng: go run cmd/api/main.go

API Endpoints

  • GET /api/v1/books: Lấy tất cả sách

  • GET /api/v1/books/:id: Lấy sách theo ID

  • POST /api/v1/books: Tạo sách mới

  • PUT /api/v1/books/:id: Cập nhật thông tin sách

  • DELETE /api/v1/books/:id: Xóa sách

Dự án này tuân theo các nguyên tắc thiết kế phần mềm: tách biệt các thành phần logic, sử dụng interfaces để dependency injection, và chuẩn hóa các phản hồi API. Bạn có thể dễ dàng mở rộng nó bằng cách thêm middleware authentication, validation logic phức tạp hơn, hoặc thay thế in-memory repository bằng cơ sở dữ liệu thực như MySQL hoặc MongoDB.

Series này đã khá dài vì vậy tôi kết thúc series, tôi sẽ viết một số bài chi tiết về các ứng dụng đơn giản, mong được các bạn ủng hộ, source code tôi đẩy lên đây

0
Subscribe to my newsletter

Read articles from Eminel directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Eminel
Eminel

Hello, my name is Eminel a software engineer specializing in scalable software architecture, microservices, and AI-powered platforms. Passionate about mentoring, performance optimization, and building solutions that drive business success.