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


📚 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 modelM 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 conditionGoroutine 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ì?
make
là hà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
?
new | make |
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ào | Chỉ dùng cho: slice, map, channel |
Giá trị khởi tạo là zero | Giá 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ệnh | Mô tả |
go mod init <module> | Tạo file go.mod và khởi tạo module |
go mod tidy | Dọ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
Tạo cấu trúc thư mục như đã mô tả ở trên
Khởi tạo module:
go mod init go-bookstore
Cài đặt dependencies:
go get
github.com/gin-gonic/gin
github.com/google/uuid
Chạy ứng dụng:
go run cmd/api/main.go
- API sẽ chạy ở
http://localhost:8080
- API sẽ chạy ở
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
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.