0

TỔNG HỢP 100 CÂU HỎI PHỎNG VẤN GOLANG THƯỜNG GẶP

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ị (mà không block bên gửi) cho đến khi buffer đầy. Nó không yêu cầu phải luôn có một receiver sẵn sàng để nhận và xử lý dữ liệu.

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")
defer fmt.Println("hi")
fmt.Println("hello")
// Output: hello hi 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 newmake là gì?

  • new cấp phát bộ nhớ và trả về con trỏ đến đó.
    user := new(User)// equivalent to &User{}
    p := new(int) // p is *int, points to a zero-initialized int (0)
    
  • make cấp phát và khởi tạo bộ nhớ cho slices, maps, và channels (trả về giá trị chứ không phải con trỏ).
    m := make(map[string]int) // m is a map[string]int, initialized and ready for use
    s := make([]int, 5)       // s is a slice of int with length 5 and zeroed values
    ch := make(chan int)     // ch is a channel for int
    

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 (collection) 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 (package level). Nó được thực thi trước hàm main.

26. Có thể có nhiều hàm init không?

Có thể, 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 (Cách này linh hoạt hơn việc dùng Go Path trong các dự án cũ trước đây).

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) của các hàm, biến trong Go thế nào?

  • 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:= 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 giúp kích hoạt constant sau nó tự động tăng thêm 1. (Thường dùng trong việc khai báo dữ liệu dạng Enum)

43. go test là gì?

go test được sử dụng để chạy các unit test được 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 tên hàm bằng Test nằm trong file dạng _test.go , và có thể sử dụng với built in package testing:

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 -bench=..

46. Làm thế nào để viết một hàm benchmark?

Các hàm benchmark phải bắt đầu tên hàm 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 nhất định.

48. Làm thế nào để thiết lập một build constraint?

Đặt constraint trong một comment ở đầu file:

 // +build linux

Lệnh trên chỉ include file được chỉ định trong trường hợp target build là Linux, hữu ích với platform-specific code, ví dụ OS-specific APIs.

49. Slices được backed bởi arrays nghĩa là gì?

Về bản chất slice được xây dựng trên underlying array và cung cấp một giao diện linh hoạt hơn khi làm việc với các phần tử bên dưới.

50. Garbage Collection trong Go là gì?

Go tự động quản lý bộ nhớ bằng cách sử dụng garbage collection (GC), giúp giải phóng bộ nhớ không còn được sử dụng.

51. Mục đích của 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. (thường dùng các method context.WithTimeout, context.WithCancelhỗ trợ đóng goroutines một cách hiệu quả)

52. Làm thế nào để sử dụng context trong Go?

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

Lúc này goroutines có thể được đóng với channel context.Done() khi cancel() được gọi.

go func(c context.Context){
    for{
        select {
            case msg := <- someChannel:
                        fmt.Println("received msg: ",msg)
            case <-c.Done():
                        return
        }
    }
}(ctx)

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. Mục đích của sync.Mutex là gì??

sync.Mutex cung cấp cơ chế khóa để bảo vệ tài nguyên chia sẻ (shared resouces) khỏi truy cập đồng thời (race condition).

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 thế nào với channels?

select được sử dụng để xử lý nhiều hoạt động trên channel, đồng bộ hóa việc kết nối của goroutine trên nhiều channels một lúc (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à 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ể được thực thi. (cẩn trọng khi dùng default case khi select đặt trong for vì có thể dẫn đến constant default running và unnessary resource consumption)

64. Mục đích 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)
 
 go func(){
     for {
             select {
                 case <-ticker.C:
                       fmt.Println("Tick...")
             }
     }
 }()

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 vetlà 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:

 go func() {
     fmt.Println("Hello")
 }()

68. Sự khác biệt giữa ==reflect.DeepEqual() là gì?

  • == kiểm tra tính bằng nhau cho các kiểu dữ liệu nguyên thủy (primitive type).
  • 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 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.


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

// Square function reads from the input channel, squares the number, and sends it to the output channel.
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() {
    // Create a pipeline: generator -> square.
    numbers := generator(2, 3, 4)
    squares := square(numbers)

    // Consume the output from the last stage.
    for sq := range squares {
        fmt.Println(sq)
    }
}

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 dựa trên một kiểu dữ liệu có sẵn (primitive type)

type MyInt = int
type MyInt2 int

75. Sự khác biệt giữa appendcopy trong slices là gì?

  • append thê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 mapstruct 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 mocktest.

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 sẽ giúp 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.Mutexsync.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 hoặc go test -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 được sử dụng để tái sử dụng các objects và giảm áp lực lên GC. Nó cung cấp một cách để lưu trữ các objects có thể tái sử dụng, khác với GC tự động giải phóng bộ nhớ không sử dụng.

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. (Chỉ dùng trong một số trường hợp cần thiết vì reflect slow performance so với compiled type thông thường)

93. Sự khác biệt giữa bufferedunbuffered 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 panicerror 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.Readerio.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 value interface là gì và tại sao nó có thể gây lỗi?

nil value 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 check nil, 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

Để handle trường hợp trên ta có thể dùng interface assertion như sau.

	if v, ok := i.(map[string]int); ok && v != nil {
		fmt.Printf("value not nil: %v\n", v)
	}

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ặc easyjson để encode/decode nhanh hơn so với encoding/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ủa json.Encoder hoặc json.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 GOMAXPROCSruntime.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.

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí