Gần đây mình có xem qua về Golang Assessment trên LinkedIn, nếu các bạn qua được bài test này thì profile của bạn sẽ có thêm badge, và huy hiệu này giúp bạn trở nên nổi bật hơn về lĩnh vực (Golang) của mình trong mắt của recruiters và các công ty đang tuyển dụng.
Các câu hỏi khá thú vị và bao quát nhiều khía cạnh về ngôn ngữ này, nếu các bạn chưa thử sức thì có thể vào làm bây giờ vì bài viết này sẽ spoiler kha khá câu hỏi trong đó (nếu LinkedIn chưa cập nhật).
Các câu hỏi đều bằng tiếng Anh, mình không tiện dịch sang tiếng Việt vì có thể mất hoặc sai nghĩa của nó.
0. Tác giả
Mình thường xuyên update các bài viết insights trên LinkedIn và blog Devtrovert
1. What do you need for two functions to be the same type?
A. They should share the same signatures, including parameter types and return types.
B. They should share the same parameter types but can return different types.
C. All functions should be the same type.
D. The functions should not be a first class type.
2 hàm được gọi là cùng loại nếu chúng chia sẻ chung (hay có cùng) signature bao gồm: các tham số của nó, kiểu trả về.
Ở bên dưới, functionA và functionB cùng loại, chúng đều có thể assign cho biến x có kiểu sigFunc
:
type sigFunc func(a int, b float64) (bool, error) func functionA(a int, b float64) (bool, error) { return true, nil
} func functionB(a int, b float64) (bool, error) { return false, nil
} func main() { var x sigFunc = functionA x = functionB print(x)
}
2. What does the len()
function return if passed a UTF-8 encoded string?
A. the number of characters B. the number of bytes C. It does not accept string types. D. the number of code points
Một điều khá thú vụ trong Go, bản chất của chuỗi (string) là byte array, tức là khi chúng ta đưa một kí tự của Ấn Độ, hay Trung Quốc, chúng ta sẽ tính bytes của chúng thay vì tính đó là 1 ký tự.
Nếu muốn tính số lượng ký tự, bản có thể sử dụng utf8.RuneCountInString
:
func main() { s := "世界" fmt.Println("Byte length:", len(s)) // 6 bytes fmt.Println("Rune count:", utf8.RuneCountInString(s)) // 2 letters
}
3. What is the value of Read
?
const ( Write = iota Read Execute
) A. 0
B. 1
C. 2
D. a random value
iota là một keyword khá thú vị, nó đại diện cho dãy số tăng dần xuất phát từ 0 và tăng dần, cho mỗi item trong một const block
nếu bạn viết đúng như ở code mẫu trên.
Nếu bạn hứng thú về cách sử dụng iota, cũng như một vài trick thì có thể tham khảo bài viết này: Go Enums: The Right Way to Implement, Iterate and Namespacing Trick.
4. How do you tell go test
to print out the tests it is running?
A. go test
B. go test -x
C. go test --verbose
D. go test -v
Khi sử dụng go test
trên command line tool, để hiển thị kết quả test chi tiết, chúng ta sử dụng -v
và đó là syntax đúng.
go test -v
Khi run câu lệnh với cờ -v (verbose), bạn sẽ thấy được tên của mỗi test, kết quả của nó (PASS/ FAIL) cùng với thời gian thực thi test, nếu test được thực thi quá ngắn thì thời gian sẽ hiện 0.00s:
=== RUN TestAdd
=== RUN TestAdd/case_1_2_3
=== RUN TestAdd/case_-1_-2
=== RUN TestAdd/case_0
=== RUN TestAdd/case_-1_2
--- PASS: TestAdd (0.00s) --- PASS: TestAdd/case_1_2_3 (0.00s) --- PASS: TestAdd/case_-1_-2 (0.00s) --- PASS: TestAdd/case_0 (0.00s) --- PASS: TestAdd/case_-1_2 (0.00s)
PASS
ok command-line-arguments 0.523s
Ví dụ trên được mình trích ra từ bài viết Take Golang Testing Beyond the Basics.
5. What does a sync.Mutex
block while it is locked?
A. all goroutines
B. any other call to lock that Mutex
C. any reads or writes of the variable it is locking
D. any writes to the variable it is locking
Trong Go, sync.Mutex hay mutual exclusion có thể đã quá quen thuộc với các bạn vì nó được ứng dụng ở các ngôn ngữ khác rất nhiều, thiết kế lock này đảm bảo chỉ duy nhất 1 goroutine có thể truy cập vào phần code được lock.
Kết quả của bài này là B, khi các goroutine khác gọi mtx.Lock()
khi mutex này đã được gọi, nó sẽ đứng hẳn ở đấy và chờ mutex này được nhả (unlock).
Nếu bản hỏi tại sao C không phải là câu trả lời thì mtx.Lock() thường được dùng để bảo vệ một shared variable, tuy nhiên đó là mục đích trừu tượng của nó, về khía cạnh technical thì nó không cần bảo vệ bất kì biến nào.
var mtx = sync.Mutex{} func Add() { mtx.Lock() defer mtx.Unlock() a++
}
Ví dụ ở trên cũng được lấy ra từ một article khác đi sâu hơn về sync package: Go Sync Package: 6 Key Concepts for Concurrency
6. What is an idiomatic way to pause execution of the current scope until an arbitrary number of goroutines have returned?
A. Pass an int and Mutex to each and count when they return.
B. Loop over a select statement.
C. Sleep for a safe amount of time.
D. sync.WaitGroup
Ở đây, người ta hỏi đâu là cách thường thấy để tạm hoãn sự thực thi của một hàm để chờ các goroutines khác hoàn tất.
Nếu bạn đã đọc về bài sync package mình giới thiệu ở trên, bạn hoàn toàn có thể chọn ngay sync.WaitGroup
, time.Sleep
là cách chúng ta sleep để chờ nhưng không hề biết goroutines khác đã xong hay chưa.
func main() { wg := sync.WaitGroup{} for i := 0; i < 500; i++ { wg.Add(1) go func() { defer wg.Done() Add() }() } wg.Wait() fmt.Println(a)
}
Ở ví dụ trên, sync.WaitGroup
sẽ chờ cho toàn bộ các goroutines hoàn thành thông qua ba hàm cơ bản của nó, wg.Add(..)
, wg.Done()
, wg.Wait()
.
Cách nó hoạt động khá đơn giản, trước khi thực hiện goroutine, mình tăng counter của waitGroup lên 1 bằng wg.Add(1)
, sau khi goroutine thực hiện xong thì wg.Done()
sẽ giảm counter xuống 1, đồng thời wg.Wait()
sẽ chờ cho counter về 0.
7. What is a side effect of using time.After
in a select
statement?
A. It blocks the other channels.
B. It is meant to be used in select statements without side effects.
C. It blocks the select statement until the time has passed.
D. The goroutine does not end until the time passes.
Nếu bạn không biết về time.After, thì nó là một hàm trong time
package nhận vào Duration
và trả về một channel, channel này sẽ emit value sau khoảng thời gian mà bạn nhập vào:
func After(d Duration) <-chan Time
Hàm này thường được sử dụng với câu lệnh select
để thực hiện timeout hoặc delay, ví dụ sau sẽ chờ done channel bằng time.After
, nếu done
channel không trả về gì trong vòng 3s thì chúng ta sẽ timeout:
func main() { timeout := 3 * time.Second start := time.Now() done := make(chan bool) select { case <-done: fmt.Println("Operation completed.") return case <-time.After(timeout): fmt.Printf("Timeout after %v\n", time.Since(start)) }
}
Nào, quay trở lại câu hỏi và nói về tác dụng phụ của hàm này.
Đối với các time.After
ngắn hạn, nó không phải là vấn đề, nhưng giả sử chúng ta set duration cho time.After
là 1 tiếng như ở ví dụ dưới đây, khi done
channel đã trả về, time.After vẫn còn trong memory của chúng ta tới tận 1 tiếng sau.
func main() { done := make(chan bool) go func() { time.Sleep(500 * time.Millisecond) done <- true }() for { select { case <-done: fmt.Println("Operation completed.") return case <-time.After(time.Hour): fmt.Println("Still waiting...") } }
}
Và đồng thời, goroutine được tạo bởi time.After
sẽ không bị thu hồi, trong khi chúng ta hoàn toàn có thể xoá nó đi bởi done
channel đã trả về và chúng ta xong việc.
8. What restriction is there on the type of var
to compile this i := myVal.(int)?
A. myVal must be an integer type, such as int, int64, int32, etc.
B. myVal must be able to be asserted as an int.
C. myVal must be an interface.
D. myVal must be a numeric type, such as float64 or int64.
Type assertion trong Go đòi hỏi myVal phải là interface, và đáp án là C.
Tuy nhiên, cách dùng trong câu hỏi rất nguy hiểm, thông thường khi dùng type assertion, chúng ta sẽ sử dụng 2-value form
để tránh bị panic nếu interface không phải int và xử lý chúng cẩn thận hơn:
// BAD
i := myVal.(int) // BETTER
i, ok := myVal.(int)
// ... doing something with ok
9. What is the correct way to pass this as a body of an HTTP POST request?
data := "A group of Owls is called a parliament" A. resp, err := http.Post("https://httpbin.org/post", "text/plain", []byte(data))
B. resp, err := http.Post("https://httpbin.org/post", "text/plain", data)
C. resp, err := http.Post("https://httpbin.org/post", "text/plain", strings.NewReader(data))
D. resp, err := http.Post("https://httpbin.org/post", "text/plain", &data)
Để send data dưới hình thức là body của HTTP POST requests, điều quan trọng chúng ta cần phải biết là content-type, tuy nhiên 4 câu trả lời ở trên đều text/plain nên chúng ta cần một Reader
, hay chính xác hơn là io.Reader
, đây là một interface cơ bản của Go để đọc string/ text từ nhiều nguồn khác nhau thay vì string
hay []byte
trong memory.
Post(url string, contentType string, body io.Reader) (resp *http.Response, err error)
The Reader
interface is defined as follows:
type Reader interface { Read(p []byte) (n int, err error)
}
Và để giải quyết yêu cầu này, chúng ta sẽ convert body sang buffer và implement interface này:
func main() { data := "A group of Owls is called a parliament" contentType := "text/plain" body := strings.NewReader(data) // or // body := bytes.NewBufferString(data) resp, err := http.Post("https://example.com", contentType, body) // ....
}
Vì vậy cách đúng để giải quyết câu hỏi trên là:
resp, err := http.Post("https://httpbin.org/post", "text/plain", strings.NewReader(data))
10. What should the idiomatic name be for an interface with a single method and the signature Save() error
?
A. Saveable B. SaveInterface C. ISave D. Saver
Nếu các bạn từng đọc về Effective Go, chúng ta đều hết các single-method interface thường được đặt tên với "-er" suffix sau function name như Reader
, Writer
, Formatter
, and CloseNotifier
..
Vì vậy kết quả của câu hỏi trên là Saver.
Có thể bạn sẽ biết về một interface khá nổi khi chúng ta sử dụng fmt.Println()
là Stringer
:
type Stringer interface { String() string
}
Cái này là trick tương tự như @override toString()
của các ngôn ngữ khác khi làm việc với các hàm để in ra console như fmt.Println()
.
11. What is the default case sensitivity of the JSON Unmarshal
function?
A. The default behavior is case insensitive, but it can be overridden.
B. Fields are matched case sensitive.
C. Fields are matched case insensitive.
D. The default behavior is case sensitive, but it can be overridden.
Hàm JSON Unmarshal trong Go mặc định sẽ là case-insensitive, không phân biệt hoa thường, tuy nhiên chúng ta có thể override lại behavior này của json bằng json
struct tag, tuy nhiên nếu bạn có 2 field là title và Title thì sao?
Trước khi trả lời câu hỏi này, chúng ta chứng minh cho câu trả lời của câu hỏi trên:
type Post struct { Title string `json:"Title"` SubTitle string
} func main() { p := Post{} json.Unmarshal([]byte(`{"title":"hello","subtitle":"world"}`), &p) fmt.Println(p)
} // {hello world}
Trong ví dụ này, rõ ràng chúng to định nghĩa tường mình json struct tag là Title
, viết hoa chữ cái đầu, hoặc Subtitle
cũng viết hoa chữ cái đầu nhưng không có struct tag, json.Unmarshal vẫn hoàn thành tốt nhiệm vụ của nó.
Nếu bạn không quen thuộc với encoding/json
package, bạn có thể đọc thêm ở Go JSON: ALL You Need To Use encoding/json Effectively
"Tại sao mình định nghĩa struct tag là "Title" mà nó vẫn match với "title"?"
encoding/json
có cơ chế fallback:
-
Đầu tiên nó tìm bằng "exact match" giữa JSON field name và struct tag.
-
Nếu không tìm thấy bằng "exact match", nó sẽ chuyển sang case-insentive match và lưu ý rằng bước này tốn performance (linear search).
type Post struct { Title string SubTitle string TitleSm string `json:"title"`
} func main() { p := Post{} json.Unmarshal([]byte(`{"title":"hello","subtitle":"world"}`), &p) fmt.Println(p)
} // { world hello}
// p.Title is empty
12. Where is the built-in recover method useful?
A. in the main function
B. immediately after a line that might panic
C. inside a deferred function
D. at the beginning of a function that might panic
Hàm recover của Go chỉ các tạc dụng ở bên trong deferred function, nếu bạn gọi nó trực tiếp bằng defer thì không có tác dụng gì:
// BAD
func main() { defer recover() panicCode() // <-- crash
} // BETTER
func handlePanic() { if panicInfo := recover(); panicInfo != nil { fmt.Println(panicInfo) }
} func main() { defer handlePanic() panicCode()
}
Ví dụ trên được trích từ bài viết "Go Panic & Recover: Don’t Make These Mistakes
13. What is the difference between the time
package’s Time.Sub()
and Time.Add()
methods?
A. Time.Add() is for performing addition while Time.Sub() is for nesting timestamps.
B. Time.Add() always returns a later time while time.Sub always returns an earlier time.
C. They are opposites. Time.Add(x) is the equivalent of Time.Sub(-x).
D. Time.Add() accepts a Duration parameter and returns a Time while Time.Sub() accepts a Time parameter and returns a Duration.
Điểm khác biệt rõ rệt giữa Time.Add()
và time.Sub()
trong time
package nằm ở tham số và giá trị trả về của chúng:
Time.Add(...)
nhận vàotime.Duration
và trả vềtime.Time
Time.Sub(...)
nhận vàotime.Time
và trả vềtime.Duration
_"Tại sao chúng không nhận vào Duration và trả về time.Time? Điều gì làm nên khác biệt?"
Nguyên nhân bởi Time.Add()
có thể xử lý được cả số âm, và khá hiệu quả khi chúng ta muốn trừ duration tại thời điểm nhất định. Tương tự, rất vô nghĩa nếu chúng ta có một Time.Sub
cũng hoạt động tương tự thế bởi 2 hàm cùng 1 tính năng.
Để hiểu rõ hơn, bạn có thể xem ví dụ sau:
func main() { now := time.Now() newTime := now.Add(2 * time.Hour) fmt.Println("Time after 2 hours:", newTime) newTime = now.Add(-2 * time.Hour) fmt.Println("Time before 2 hours:", newTime) duration := newTime.Sub(now) fmt.Println("Duration newTime to now:", duration)
} Time after 2 hours: 2023-05-09 03:05:03.177199 +0700 +07 m=+7200.000587876
Time before 2 hours: 2023-05-09 03:05:03.177199 +0700 +07 m=+7200.000587876
Duration newTime to now: 2h0m0s
Bạn có thể thấy rằng, Time.Add()
dùng để thêm hoặc bớt đi một khoảng thời gian tại thời điểm xác địnhh, trong khi Time.Sub()
sử dụng để tính khoảng thời gian giữa 2 thời điểm.
14. What is the risk of using multiple field tags in a single struct?
A. Every field must have all tags to compile.
B. It tightly couples different layers of your application.
C. Any tags after the first are ignored.
D. Missing tags panic at runtime.
Một trong những vấn đề quan trọng khi sử dụng nhiều struct tags là nó gắn kết nhiều layers của app chúng ta, để hiểu rõ hơn, hay xem ví dụ sau:
type Post struct { Title string `json:"title" bson:"title"` SubTitle string `json:"subtitle" bson:"subtitle"`
}
Ở ví dụ này, struct Post
có 2 fields, mỗi fields có 2 struct tags là json
và bson
, 2 tags này được sử dụng cho 2 layer khác nhau là network api và storage.
Như vậy chúng ta đã gắn kết giữa API và Database layer lại, nếu chúng ta sửa title
thành shortTitle
chẳng hạn, bạn sẽ phải sửa cả API và Database để đảm bảo chúng đều trỏ về 1 field duy nhất, nếu khác nhau, chúng sẽ gây khó hiểu và giảm maintainable system.
Tuy nhiên cách làm này không có gì sai, chúng ta chấp nhận sự coupling giữa các layers bởi chúng không đóng góp quá nhiều vào system
15. If you iterate over a map in a for range loop, in which order will the key:value pairs be accessed?
A. in pseudo-random order that cannot be predicted
B. in reverse order of how they were added, last in first out
C. sorted by key in ascending order
D. in the order they were added, first in first out
Khi duyệt qua một map bằng for range trong Go, thứ tự của các cặp key-value là không thể xác định và không đảm bảo, chúng cũng không theo thứ tự khi add vào map.
func main() { m := map[string]int{} m["a"] = 1 m["b"] = 2 m["c"] = 3 for key, value := range m { fmt.Printf("Key: %s, Value: %d\n", key, value) } for key, value := range m { fmt.Printf("Key: %s, Value: %d\n", key, value) }
} Key: a, Value: 1
Key: b, Value: 2
Key: c, Value: 3
Key: c, Value: 3
Key: a, Value: 1
Key: b, Value: 2
Bạn có thể thấy kết quả ở trên, trong khi chúng ta duyệt cùng 1 map, nó có thể ra kết quả khác nhau.
Bài viết được dịch từ 15 Go Interview Questions from the LinkedIn Assessment — Detailed Explanations