100 Câu hỏi thường gặp khi phỏng vấn Golang
1. Golang là gì?
Go, hay Golang, là một ngôn ngữ lập trình mã nguồn mở được phát triển bởi Google. Nó là ngôn ngữ biên dịch, kiểu tĩnh và được thiết kế để xây dựng các ứng dụng mở rộng và hiệu suất cao.
2. Các tính năng chính của Go là gì?
- Hỗ trợ concurrency bằng cách sử dụng Goroutines.
- Garbage collection.
- Statically typed và dynamic behavior.
- Cú pháp đơn giản.
- Biên dịch nhanh.
3. Goroutines là gì?
Goroutines là các thread nhẹ được quản lý bởi runtime của Go. Chúng là các function hoặc method chạy đồng thời với các function hoặc method khác.
4. Làm thế nào để tạo một Goroutine?
Sử dụng từ khóa go
trước một function:
go myFunction()
5. Channel trong Go là gì?
Channels là cách để các Goroutines giao tiếp với nhau và đồng bộ hóa việc thực thi. Chúng cho phép gửi và nhận các giá trị.
6. Làm thế nào để khai báo một channel?
ch := make(chan int)
7.Buffered channel là gì?
Buffered channel có một dung lượng xác định và cho phép gửi các giá trị cho đến khi buffer đầy. Nó không yêu cầu một receiver phải sẵn sàng để nhận.
8. Làm thế nào để đóng một channel?
Sử dụng hàm close()
:
close(ch)
9. Struct trong Go là gì?
Struct là một kiểu dữ liệu do người dùng định nghĩa cho phép nhóm các trường có kiểu dữ liệu khác nhau vào một thực thể duy nhất.
10. Làm thế nào để định nghĩa một struct?
type Person struct { Name string Age int
}
11. Interface trong Go là gì?
Interface trong Go là một kiểu dữ liệu định nghĩa tập hợp các method signatures. Nó tạo tính đa hình polymorphism bằng cách cho phép đa dạng hóa việc định nghĩa các hành vi.
12. Làm thế nào để triển khai một interface?
Một kiểu dữ liệu triển khai một interface bằng cách triển khai (implement) tất cả các method của nó:
type Animal interface { Speak() string } type Dog struct{} func (d Dog) Speak() string { return "Woof!" }
13. Từ khóa defer
là gì?
defer
được sử dụng để hoãn việc thực thi một hàm cho đến khi surrounding function trả về.
14. defer
hoạt động như thế nào?
Các hàm defer
được thực thi theo thứ tự LIFO (Last In, First Out):
defer fmt.Println("world")
fmt.Println("hello")
// Output: hello world
15. Pointer trong Go là gì?
Pointer giữ địa chỉ bộ nhớ (memory address) của một giá trị. Nó được sử dụng để truyền tham chiếu (references) thay vì sao chép giá trị (copying values).
16. Làm thế nào để khai báo một pointer?
var p *int
p = &x
17. Sự khác biệt giữa new
và make
là gì?
new
cấp phát bộ nhớ nhưng không khởi tạo giá trị.make
cấp phát và khởi tạo bộ nhớ cho slices, maps, và channels.
18. Slice trong Go là gì?
Slice là một mảng có kích thước động cho phép làm việc linh hoạt hơn với các chuỗi phần tử (sequences of elements).
19. Làm thế nào để tạo một slice?
s := make([]int, 0)
20. Map trong Go là gì?
Map là một tập hợp các cặp key-value.
21. Làm thế nào để tạo một map?
m := make(map[string]int)
22. Câu lệnh select
là gì?
select
cho phép một Goroutine đợi (wait) trên nhiều hoạt động giao tiếp (communication operations).
23. Làm thế nào để sử dụng select
?
select { case msg := <-ch: fmt.Println(msg) default: fmt.Println("No message received") }
24. Channel nil
là gì?
Channel nil
sẽ chặn cả việc gửi và nhận.
25. Hàm init
là gì?
init
là một hàm đặc biệt để khởi tạo các biến cấp package. Nó được thực thi trước main
.
26. Có thể có nhiều hàm init
không?
Có, nhưng chúng sẽ được thực thi theo thứ tự xuất hiện.
27. Struct rỗng {}
là gì?
Struct rỗng tiêu thụ (consumes) 0 byte bộ nhớ (storage).
28. Làm thế nào để xử lý lỗi trong Go?
Bằng cách trả về kiểu error
và kiểm tra nó:
if err != nil { return err
}
29. Type assertion là gì?
Type assertion được sử dụng để lấy giá trị thực sự của một interface:
value, ok := x.(string)
30. Lệnh go fmt
là gì?
go fmt
format mã nguồn Go theo standard style.
31. Mục đích của go mod
là gì?
go mod
quản lý các module dependencies trong các dự án Go.
32. Làm thế nào để tạo một module?
go mod init module-name
33. Package trong Go là gì?
Package là cách nhóm các file Go liên quan lại với nhau.
34. Làm thế nào để import một package?
import "fmt"
35. Quy tắc hiển thị (visibility rules) trong Go là gì?
- Các identifier được export bắt đầu bằng chữ cái viết hoa.
- Các identifier không được export (private) bắt đầu bằng chữ cái viết thường.
36. Sự khác biệt giữa var
và :=
là gì?
var
dùng để khai báo biến với kiểu rõ ràng (explicit types).:=
dùng để khai báo biến một cách ngắn gọn với kiểu ngầm định (inferred types).
37. panic
trong Go là gì?
panic
được dùng để kết thúc chương trình ngay lập tức khi gặp lỗi.
38. recover
là gì?
recover
được dùng để lấy lại quyền kiểm soát sau khi xảy ra panic
.
39. Làm thế nào để sử dụng recover
?
Nó được sử dụng trong hàm defer
:
defer func() { if r := recover(); r != nil { fmt.Println("Recovered:", r) }
}()
40. Constant trong Go là gì?
Constants là các giá trị không thể thay đổi, được khai báo bằng từ khóa const
.
41. Làm thế nào để khai báo một constant?
const Pi = 3.14
42. iota trong Go là gì?
iota
là một lệnh tạo constant tự động tăng thêm 1.
43. go test
là gì?
go test
được sử dụng để chạy các unit test viết trong Go.
44. Làm thế nào để viết một hàm test?
Các hàm test phải bắt đầu bằng Test
:
func TestAdd(t *testing.T) { result := Add(2, 3) if result != 5 { t.Errorf("expected 5, got %d", result) }
}
45. Benchmarking trong Go là gì?
Benchmarking được sử dụng để đo lường hiệu suất của một hàm khi sử dụng go test
.
46. Làm thế nào để viết một hàm benchmark?
Các hàm benchmark phải bắt đầu bằng Benchmark
:
func BenchmarkAdd(b *testing.B) { for i := 0; i < b.N; i++ { Add(2, 3) } }
47. Build constraint là gì?
Build constraints được sử dụng để bao gồm (include) hoặc loại bỏ (exclude) các file khỏi quá trình build (build process) dựa trên các điều kiện.
48. Làm thế nào để thiết lập một build constraint?
Đặt constraint trong một comment ở đầu file:
// +build linux
49. Slices được backed bởi arrays là gì?
Slices được xây dựng trên các arrays và cung cấp một giao diện linh hoạt hơn để làm việc với các dãy số.
50. Garbage Collection trong Go là gì?
Go tự động quản lý bộ nhớ bằng cách sử dụng garbage collection, giúp giải phóng bộ nhớ không còn được sử dụng.
51. Package context
trong Go là gì?
Package context
được sử dụng để quản lý thời hạn (deadlines), tín hiệu hủy bỏ (cancellation signals), và các giá trị theo phạm vi yêu cầu (request-scoped values). Nó giúp kiểm soát luồng của các Goroutine và tài nguyên.
52. Làm thế nào để sử dụng context
trong Go?
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
53. sync.WaitGroup
là gì?
sync.WaitGroup
được sử dụng để chờ một tập hợp các Goroutine hoàn thành việc thực thi.
54. Làm thế nào để sử dụngsync.WaitGroup
?
var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() // Do some work }() wg.Wait()
55. sync.Mutex
là gì??
sync.Mutex
cung cấp cơ chế khóa để bảo vệ tài nguyên chia sẻ khỏi truy cập đồng thời.
56. Làm thế nào để sử dụng sync.Mutex
?
var mu sync.Mutex mu.Lock() // critical section mu.Unlock()
57. select
được sử dụng để làm gì với channels?
select
được sử dụng để xử lý nhiều hoạt động trên channel cùng một lúc, cho phép một Goroutine chờ đợi nhiều hoạt động giao tiếp (communication operations).
58. go generate
là gì??
go generate
là một lệnh để tạo mã nguồn. Nó đọc các comment đặc biệt trong mã nguồn để thực thi các lệnh cần thiết.
59. Method receiver trong Go là gì?
Method receiver xác định kiểu (type) mà method được liên kết đến, có thể là bằng giá trị (value) hoặc con trỏ (pointer):
func (p *Person) GetName() string { return p.Name }
60. Sự khác biệt giữa value receiver và pointer receiver là gì?
- Value receiver nhận một bản sao của giá trị gốc.
- Pointer receiver nhận một tham chiếu (reference) đến giá trị gốc, cho phép thay đổi giá trị gốc (original value).
61. Variadic function là gì?
Variadic function chấp nhận một số lượng đối số tùy ý không cố định:
func sum(nums ...int) int { total := 0 for _, num := range nums { total += num } return total
}
62. rune
trong Go là gì?
Rune
là một alias cho int32
và đại diện cho một mã Unicode.
63. select
block không có default
case hoạt động thế nào?
Select
block không có default
sẽ chờ cho đến khi một trong các case của nó có thể tiến hành.
64. Ticker
trong Go là gì?
A ticker
gửi các sự kiện theo khoảng thời gian đều đặn:
ticker := time.NewTicker(time.Second)
65. Làm thế nào để xử lý JSON trong Go?
Sử dụng package encoding/json
để marshal và unmarshal JSON:
jsonData, _ := json.Marshal(structure) json.Unmarshal(jsonData, &structure)
66. go vet
là gì?
go vet
kiểm tra mã nguồn Go và báo cáo các lỗi tiềm ẩn, tập trung vào các vấn đề không được trình biên dịch (compiler) phát hiện.
67. Anonymous function trong Go là gì?
Anonymous function là một hàm không có tên và có thể được định nghĩa trực tiếp:
func() { fmt.Println("Hello") }()
68. Sự khác biệt giữa ==
và reflect.DeepEqual()
là gì?
==
kiểm tra tính bằng nhau cho các kiểu dữ liệu nguyên thủy.reflect.DeepEqual()
so sánh tính bằng nhau sâu của các kiểu dữ liệu phức tạp như slices, maps, và structs.
69. time.Duration
trong Go là gì?
time.Duration
đại diện cho thời gian đã trôi qua giữa hai điểm và là một kiểu của int64
.
70. Làm thế nào để xử lý timeout với context
?
Sử dụng context.WithTimeout
để đặt một thời gian chờ:
ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel()
71. Pipeline trong Go là gì?
Pipeline là một chuỗi các giai đoạn kết nối bằng channels, trong đó mỗi giai đoạn là một tập hợp các Goroutine nhận giá trị từ upstream và gửi giá trị đến downstream.
72.Quy ước thư mục pkg
trong Go là gì?
pkg
là một thư mục được sử dụng để đặt các package có thể tái sử dụng. Đây là một quy ước phổ biến nhưng không bắt buộc bởi Go.
73. Làm thế nào để debug mã Go?
Sử dụng các công cụ như dlv
(Delve), các câu lệnh print, hoặc package log
.
74. type
alias trong Go là gì?
type
aliasing cho phép bạn tạo một tên mới cho một kiểu dữ liệu có sẵn
type MyInt = int
75. Sự khác biệt giữa append
và copy
trong slices là gì?
append
hêm các phần tử vào một slice và trả về một slice mới.copy
sao chép các phần tử từ một slice này sang một slice khác.
slice1 := []int{1, 2}
slice2 := []int{3, 4}
copy(slice2, slice1) // [1, 2]
76. Mục đích của go doc
là gì?
go doc
được sử dụng để hiển thị tài liệu hướng dẫn (document) cho một package, function, hoặc biến trong Go.
77. Làm thế nào để xử lý panics trong production code?
Sử dụng recover
để xử lý panics một cách đúng đắn và ghi log chúng để debugging:
defer func() { if r := recover(); r != nil { log.Println("Recovered from:", r) }
}()
78. Sự khác biệt giữa map
và struct
là gì?
map
là một cấu trúc dữ liệu động với các cặp key-value.struct
là một cấu trúc dữ liệu tĩnh với các trường cố định.
79. Package unsafe
dùng làm gì?
Package unsafe cho phép thao tác low-level memory . Không được khuyến nghị sử dụng thường xuyên.
80. Làm thế nào để thực hiện dependency injection trong Go?
Sử dụng interfaces
và các hàm constructor để truyền các dependencies, giúp dễ dàng mock
và test
.
type HttpClient interface{} func NewService(client HttpClient) *Service { return &Service{client: client}
}
81. Goroutine khác gì với một thread?
Một Goroutine là một thread nhẹ được quản lý bởi Go runtime. Nó khác với các OS threads vì sử dụng stack khởi đầu nhỏ hơn (2KB) và được ánh xạ vào nhiều OS threads. Điều này làm cho Goroutines hiệu quả hơn trong việc xử lý concurrency.
82. Go scheduler hoạt động như thế nào?
Go scheduler sử dụng một thuật toán work-stealing với M:N scheduling, trong đó M đại diện cho OS threads và N đại diện cho Goroutines. Nó lên lịch cho các Goroutines trên các OS threads và CPUs sẵn có, nhằm cân bằng khối lượng công việc để đạt hiệu suất tối ưu.
83. Memory leak là gì và làm thế nào để ngăn chặn nó trong Go?
Memory leak xảy ra khi bộ nhớ đã cấp phát không được giải phóng. Trong Go, điều này có thể xảy ra nếu các Goroutines không được kết thúc hoặc các tham chiếu (references) đến các đối tượng được giữ lại không cần thiết. Sử dụng defer
để dọn dẹp và dừng các Goroutines đúng cách để ngăn chặn memory leak.
84. Garbage collection hoạt động như thế nào trong Go?
Go sử dụng garbage collector dạng concurrent, mark-and-sweep. Nó xác định các đối tượng có thể truy cập được trong giai đoạn mark và thu thập các đối tượng không thể truy cập trong giai đoạn sweep, cho phép các Goroutines khác tiếp tục chạy trong quá trình thu gom.
85. Giải thích sự khác nhau giữa sync.Mutex
và sync.RWMutex
.
sync.Mutex
được sử dụng để cung cấp quyền truy cập độc quyền vào một tài nguyên được chia sẻ.sync.RWMutex
cho phép nhiều readers hoặc một writer tại một thời điểm, cung cấp hiệu suất tốt hơn cho các workloads nặng về đọc.
86. Race condition là gì và làm thế nào để phát hiện chúng trong Go?
Race condition xảy ra khi nhiều Goroutines truy cập một biến chia sẻ đồng thời mà không có sự đồng bộ hóa hợp lý. Sử dụng go run -race
để phát hiện các race condition trong chương trình Go.
87. Struct tag là gì và được sử dụng như thế nào?
Struct tags cung cấp metadata cho các trường (fields) trong struct, thường được sử dụng cho việc serialize JSON:
type User struct { Name string `json:"name"` Age int `json:"age"`
}
88. Làm thế nào để tạo một custom error trong Go?
Tạo một custom error bằng cách implement interface error
:
type MyError struct { Msg string
}
func (e *MyError) Error() string { return e.Msg
}
89. Nil pointer dereference là gì và làm thế nào để tránh nó?
Nil pointer dereference xảy ra khi bạn cố gắng truy cập giá trị mà một con trỏ nil
trỏ đến. Tránh điều này bằng cách kiểm tra nil
trước khi sử dụng các con trỏ.
90. Explain the difference between sync.Pool
and garbage collection
.
sync.Pool
is used for reusing objects and reducing GC
pressure. It provides a way to cache reusable objects, unlike the GC which automatically frees unused memory.
91. Làm thế nào để implement một worker pool trong Go?
Sử dụng channels
để phân phối các tasks và quản lý các Goroutines worker:
jobs := make(chan int, 100)
for w := 1; w <= 3; w++ { go worker(w, jobs)
}
92. reflect
trong Go là gì?
Package reflect
cho phép kiểm tra runtime của các loại và giá trị. Nó được sử dụng cho các thao tác động (dynamic) như kiểm tra các trường của struct hoặc các phương thức.
93. Sự khác biệt giữa buffered
và unbuffered
channels là gì?
- Một
buffered
channel có dung lượng, cho phép các Goroutines gửi dữ liệu mà không bị chặn (block or lock) cho đến khi buffer đầy. - Một
unbuffered
channel không có dung lượng và sẽ bị chặn cho đến khi receiver sẵn sàng.
94. Làm thế nào để tránh Goroutine leaks?
Đảm bảo các Goroutines được kết thúc bằng cách sử dụng context
để hủy hoặc sử dụng timeouts với channels.
95. Sự khác nhau chính giữa panic
và error
là gì?
error
được sử dụng để xử lý các tình huống dự đoán được và có thể trả về.panic
được sử dụng cho các tình huống không mong muốn và dừng luồng thực thi đột ngột.
96. Giải thích các interface io.Reader
và io.Writer
.
io.Reader
có phương thức Read
để đọc dữ liệu, trong khi io.Writer
có phương thức Write
để ghi dữ liệu. Chúng tạo cơ sở cho các trừu tượng I/O của Go.
97. nil interface
là gì và tại sao nó gây ra vấn đề?
nil interface
là một interface có giá trị mà nó tham chiếu đến là nil
. Nó có thể gây ra lỗi không mong muốn khi so sánh, vì một interface với giá trị cơ bản nil
bản thân nó lại không bằng nil
.
type MyInterface interface{}
var i MyInterface
var m map[string]int
i = m // This case, m is nil but i not nil
98. Làm thế nào để ngăn chặn deadlock trong các chương trình Go khi sử dụng goroutines?
Để ngăn chặn deadlock, hãy đảm bảo rằng:
- Các Lock luôn được acquired theo cùng một thứ tự trên tất cả các Goroutines.
- Sử dụng
defer
để giải phóng khóa (release locks). - Tránh holding lock trong khi gọi đến một hàm khác mà có tham chiếu đến cùng lock đó.
- Hạn chế việc sử dụng channels trong vùng tranh chấp đang được khóa (locked sections).
99. Làm thế nào để tối ưu hóa hiệu suất encode/decode JSON trong Go?
- Sử dụng các thư viện
jsoniter
hoặceasyjson
để encode/decode nhanh hơn so vớiencoding/json
chuẩn. - Định nghĩa trước các trường của struct bằng cách sử dụng
json:"field_name"
tags để tránh chi phí reflection. - Sử dụng
sync.Pool
để tái sử dụng các instances củajson.Encoder
hoặcjson.Decoder
khi encode/decode dữ liệu JSON lớn lặp đi lặp lại.
100. Sự khác biệt giữa GOMAXPROCS
và runtime.Gosched()
là gì?
GOMAXPROCS
điều khiển số lượng tối đa của các OS threads có thể thực thi Goroutines đồng thời. Nó cho phép điều chỉnh mức độ parallelism.runtime.Gosched()
nhường quyền xử lý cho các Goroutines khác. Nó không treo Goroutine hiện tại mà thay vào đó cho phép Go scheduler có cơ hội chạy các Goroutines khác.