01Goroutine khác gì OS thread? Làm sao chờ nhiều goroutine chạy xong?Junior
Gợi ý trả lời
Goroutine là "luồng nhẹ" do runtime Go quản lý: stack khởi đầu chỉ vài KB và tự giãn, chi phí tạo rất thấp. Scheduler của Go phân phối nhiều goroutine lên một số ít OS thread (mô hình M:N), số thread chạy song song tối đa theo
GOMAXPROCS.Chờ nhiều goroutine bằng
sync.WaitGroup:var wg sync.WaitGroup for _, u := range urls { wg.Add(1) go func() { defer wg.Done() fetch(u) }() } wg.Wait()Go 1.25 có thêm
wg.Go(func() { ... })gói gọnAddvàDone.Hay sai: gọi
wg.Add(1)bên trong goroutine, khiếnWaitcó thể trả về trước khi goroutine kịp đăng ký. Một lỗi khác: hàmmainkết thúc thì mọi goroutine bị dừng theo, không ai chờ chúng.02Array và slice khác nhau thế nào? Vì sao
appendđôi khi làm thay đổi cả slice khác?JuniorGợi ý trả lời
Array có độ dài cố định và là giá trị (gán là copy toàn bộ). Slice là một "khung nhìn" gồm con trỏ đến mảng bên dưới,
lenvàcap.- Gán hoặc cắt slice (
b := a[1:3]) không copy dữ liệu; hai slice dùng chung mảng bên dưới. appendkhi còncapsẽ ghi thẳng vào mảng chung, nên có thể ghi đè dữ liệu slice khác đang thấy. Khi hếtcap, nó cấp phát mảng mới và từ đó hai slice tách rời.
a := []int{1, 2, 3, 4} b := a[:2] b = append(b, 99) // a thành [1 2 99 4]- Muốn tách hẳn:
slices.Clone(a)hoặc dùng full slice expressiona[:2:2]để giới hạncap. - Biết trước kích thước thì
make([]T, 0, n)để tránh cấp phát lại nhiều lần.
Người phỏng vấn muốn nghe: luôn dùng giá trị trả về của
append(s = append(s, x)).- Gán hoặc cắt slice (
03Go xử lý lỗi như thế nào? Khi nào dùng
panic?errors.Isvàerrors.Asđể làm gì?JuniorGợi ý trả lời
Lỗi trong Go là giá trị kiểu
error, được trả về như kết quả thông thường và phải kiểm tra tường minh.panicchỉ dành cho tình huống không thể tiếp tục hoặc lỗi lập trình (ví dụ cấu hình bắt buộc bị thiếu lúc khởi động), không dùng cho lỗi nghiệp vụ.Bọc lỗi để giữ ngữ cảnh mà không mất lỗi gốc:
if err != nil { return fmt.Errorf("load user %d: %w", id, err) } // Ở tầng trên: if errors.Is(err, sql.ErrNoRows) { /* trả 404 */ }errors.Is: so với một lỗi cụ thể trong chuỗi bọc.errors.As: lấy ra lỗi thuộc một kiểu cụ thể để đọc field của nó.recovertrongdeferdùng ở biên (middleware HTTP) để một request panic không làm sập cả server.
Hay sai: dùng
%vthay%wkhi bọc, làmerrors.Iskhông tìm thấy lỗi gốc.04Interface trong Go khác gì Java hay C#? Vì sao một interface chứa con trỏ nil lại khác
nil?JuniorGợi ý trả lời
Go implement interface ngầm định: kiểu nào có đủ method thì tự thoả mãn interface, không cần khai báo
implements. Nhờ vậy interface thường được định nghĩa ở phía sử dụng, nhỏ gọn (io.Readerchỉ có một method), và dễ thay bằng mock khi test.Giá trị interface gồm hai phần: kiểu và giá trị. Nó chỉ bằng
nilkhi cả hai đều nil.func find() error { var err *MyError = nil return err // interface có kiểu *MyError, nên != nil }Caller kiểm tra
err != nilsẽ thấytruedù "không có lỗi".Hay sai: khai báo interface lớn ở phía cài đặt cho mọi struct, theo thói quen từ Java. Câu hay được nhắc trong cộng đồng Go: nhận vào interface, trả về struct cụ thể. Khi không có lỗi, hãy
return niltrực tiếp.05Channel buffered và unbuffered khác nhau thế nào?
selectdùng để làm gì? Khi nào xảy ra deadlock?Gợi ý trả lời
- Unbuffered: gửi bị chặn đến khi có bên nhận (và ngược lại). Đây là điểm đồng bộ giữa hai goroutine.
- Buffered (
make(chan T, n)): gửi chỉ bị chặn khi buffer đầy, nhận bị chặn khi buffer rỗng.
selectchờ trên nhiều channel cùng lúc, thường dùng cho timeout hoặc huỷ:select { case res := <-results: handle(res) case <-ctx.Done(): return ctx.Err() }Quy tắc: chỉ bên gửi đóng channel; gửi vào channel đã đóng gây panic; nhận từ channel đã đóng trả giá trị zero ngay lập tức (
v, ok := <-chđể phân biệt).for range chdừng khi channel đóng.Deadlock xảy ra khi mọi goroutine đều đang chờ nhau, ví dụ gửi vào channel unbuffered mà không có ai nhận. Runtime báo "all goroutines are asleep" nếu toàn bộ chương trình bị kẹt; kẹt một phần thì không báo gì và thành goroutine leak.
06Map trong Go có an toàn khi dùng đồng thời không? Bạn phát hiện và xử lý race condition thế nào?
Gợi ý trả lời
Không. Đọc và ghi map đồng thời có thể làm runtime dừng chương trình với lỗi "concurrent map read and map write". Các biến dùng chung khác cũng có data race nhưng thường im lặng cho kết quả sai.
Phát hiện bằng race detector:
go test -race ./...hoặcgo run -race. Nên bật trong CI.Cách xử lý:
sync.Mutexbọc map, hoặcsync.RWMutexkhi đọc nhiều hơn ghi rất nhiều.sync.Maphợp với vài trường hợp đặc thù (key ghi một lần đọc nhiều lần, hoặc mỗi goroutine dùng tập key riêng); không phải thay thế mặc định.- Bộ đếm đơn giản:
atomic.Int64. - Hoặc tránh chia sẻ: để một goroutine sở hữu dữ liệu, các goroutine khác gửi yêu cầu qua channel.
Hay sai: copy struct có chứa
sync.Mutex(truyền theo giá trị), khiến mỗi bản có một khoá riêng.go vetcảnh báo được lỗi này.07
context.Contextdùng để làm gì? Có những quy ước nào khi dùng context?Gợi ý trả lời
Context mang tín hiệu huỷ, deadline và một ít giá trị gắn với request, truyền xuyên suốt chuỗi gọi hàm. Khi client ngắt kết nối hoặc hết thời gian, mọi thao tác bên dưới (truy vấn DB, gọi HTTP) đều có thể dừng sớm.
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second) defer cancel() row := db.QueryRowContext(ctx, query, id)Quy ước:
- Context là tham số đầu tiên, tên
ctx; không lưu context trong struct. - Luôn
defer cancel()để giải phóng tài nguyên. - Goroutine chạy lâu phải kiểm tra
ctx.Done(). WithValuechỉ dùng cho dữ liệu theo request (request ID, thông tin trace), không dùng để truyền tham số tuỳ chọn.
Hay sai: dùng context của request cho việc chạy nền sau khi đã trả response. Request xong là context bị huỷ; hãy dùng
context.WithoutCancelhoặc context riêng.- Context là tham số đầu tiên, tên
08
deferhoạt động thế nào? Vòng lặpforkết hợp goroutine và closure có thay đổi gì từ Go 1.22?Gợi ý trả lời
deferhoãn lời gọi hàm đến khi hàm bao ngoài kết thúc (kể cả khi panic), chạy theo thứ tự LIFO. Tham số của lời gọi được tính ngay lúc gặpdefer, không phải lúc chạy.for i := 0; i < 3; i++ { defer fmt.Println(i) // in 2, 1, 0 }- Đừng
defer f.Close()trong vòng lặp mở hàng nghìn file: chúng chỉ đóng khi hàm kết thúc. Tách thân vòng lặp thành hàm riêng.
Về vòng lặp: trước Go 1.22, biến vòng lặp dùng chung cho mọi lần lặp nên goroutine hay closure thường thấy giá trị cuối cùng. Từ Go 1.22, mỗi lần lặp có biến riêng (khi
go.modkhai báo 1.22 trở lên), nên lỗi này không còn.Người phỏng vấn muốn nghe: bạn biết thay đổi này phụ thuộc phiên bản trong
go.mod.- Đừng
09Goroutine leak là gì? Làm sao phát hiện và phòng tránh?Senior
Gợi ý trả lời
Goroutine leak là goroutine bị chặn mãi mãi và không bao giờ kết thúc, giữ theo bộ nhớ và tài nguyên nó tham chiếu. Số goroutine tăng dần theo thời gian là dấu hiệu rõ nhất.
Nguyên nhân hay gặp:
- Gửi vào channel mà bên nhận đã bỏ đi (ví dụ đã trả về vì timeout).
- Nhận từ channel không bao giờ được gửi hoặc đóng.
- Vòng lặp chạy nền không kiểm tra
ctx.Done(). - HTTP response không đóng
Body.
ch := make(chan Result, 1) // buffer 1: goroutine gửi xong vẫn thoát được go func() { ch <- slowCall() }() select { case r := <-ch: return r, nil case <-ctx.Done(): return Result{}, ctx.Err() }Phát hiện: theo dõi
runtime.NumGoroutine(), xem profile goroutine quanet/http/pprof, dùng thư việngoleaktrong test.Người phỏng vấn muốn nghe: mỗi goroutine bạn tạo phải có câu trả lời rõ ràng cho câu hỏi "nó kết thúc khi nào?".
10Generics trong Go dùng thế nào? Khi nào nên dùng generics thay vì interface?Senior
Gợi ý trả lời
Từ Go 1.18, hàm và kiểu có thể nhận type parameter với ràng buộc (constraint):
func Map[T, U any](in []T, f func(T) U) []U { out := make([]U, 0, len(in)) for _, v := range in { out = append(out, f(v)) } return out }- Constraint có thể là
any,comparable,cmp.Ordered, hoặc interface tự định nghĩa chứa tập kiểu (~int | ~float64). - Thư viện chuẩn đã có các package generic như
slicesvàmaps.
Khi nào dùng:
- Generics: cấu trúc dữ liệu và hàm tiện ích làm việc giống hệt nhau với nhiều kiểu (cache, set, hàm lọc, phân trang).
- Interface: khi cần hành vi khác nhau cho từng kiểu (lưu vào DB hay vào file, nhiều cổng thanh toán).
Hay sai: generic hoá mọi thứ ngay từ đầu. Code Go chuộng đơn giản; hãy viết cụ thể trước, khi có hai ba chỗ lặp lại thật sự thì mới trích generic.
- Constraint có thể là
11Một service Go có độ trễ cao và tốn nhiều bộ nhớ. Bạn profile và tối ưu thế nào?Senior
Gợi ý trả lời
Đo bằng pprof trước khi sửa. Import
net/http/pprof(đặt ở cổng nội bộ) rồi dùnggo tool pprofđể xem CPU profile, heap, allocs, goroutine và block/mutex profile.Các hướng tối ưu thường gặp:
- Giảm cấp phát (allocation): cấp phát nhiều làm GC chạy nhiều. Cấp phát trước
capcho slice và map, tránh chuyển đổi[]byte/stringthừa, dùngstrings.Builder. - Escape analysis:
go build -gcflags=-mcho biết biến nào bị đẩy lên heap. sync.Poolđể tái sử dụng buffer ở đường xử lý nóng.- Tinh chỉnh GC:
GOMEMLIMITđặt giới hạn bộ nhớ mềm (hữu ích trong container),GOGCđổi tần suất GC. - Viết benchmark (
go test -bench . -benchmem) để so sánh trước và sau khi sửa.
Đừng quên ngoài code: truy vấn DB chậm, connection pool quá nhỏ, gọi API ngoài không timeout.
Người phỏng vấn muốn nghe: bạn đọc được flame graph và dựa vào số liệu, không tối ưu theo cảm giác.
- Giảm cấp phát (allocation): cấp phát nhiều làm GC chạy nhiều. Cấp phát trước
12Cần xử lý 10.000 job (gọi API ngoài) với tối đa 20 job chạy cùng lúc, dừng sớm nếu có lỗi. Bạn thiết kế thế nào?Senior
Gợi ý trả lời
Đây là bài worker pool có giới hạn concurrency. Cách gọn nhất là
errgroup(packagegolang.org/x/sync/errgroup):g, ctx := errgroup.WithContext(ctx) g.SetLimit(20) for _, job := range jobs { g.Go(func() error { return process(ctx, job) }) } if err := g.Wait(); err != nil { return err }SetLimitkhiếng.Gochặn lại khi đã đủ 20 goroutine đang chạy.- Lỗi đầu tiên sẽ huỷ
ctx; các job khác phải tôn trọngctx(truyền vào HTTP request) để dừng sớm.
Cách cổ điển: tạo 20 worker đọc từ một channel job, gửi kết quả qua channel khác, đóng channel đúng thứ tự.
Cần nghĩ thêm: rate limit phía API ngoài (
golang.org/x/time/rate), retry có backoff, có cần dừng hết khi một job lỗi hay chỉ ghi lại lỗi rồi tiếp tục.Hay sai: tạo 10.000 goroutine cùng lúc gọi API ngoài, làm cạn connection và bị đối tác chặn.
Gặp câu hỏi khác khi phỏng vấn? Kể lại ẩn danh để người sau ôn đúng trọng tâm, hoặc xem câu hỏi thật hay gặp ở nhiều công ty.