+2

📝 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ì, làm thế nào để tạo goroutine?

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.

Sử dụng từ khóa go trước một function:

go myFunction()

// With anonymous function
go func(){
}()

4. Channel trong Go là gì, làm thế nào để khai báo một channel?

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ị.

ch := make(chan int)

5.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.

 func main(){
     ch1 := make(chan int,1)
     ch1<-1 //this will not block, thanks to buffer
     
     ch2 := make(chan int)
     ch2 <- 1 // this will block main because no other goroutine will read it
 }

6. Làm thế nào để đóng một channel?

Sử dụng hàm close() (thường được gọi ở phía gửi msg vào channel, hoặc đối tượng quản lý channel đó) :

close(ch)

Và khi muốn kiểm tra xem channel đã close hay chưa

//Case 1
 value, ok := <-ch
 if !ok {
     // Channel is closed
 }
 
 //Case 2
 for value := range ch {
 // Process value
 }
 // Channel is closed when the loop exits

7. Struct trong Go là gì, thế nào để định nghĩa một struct?

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.

type Person struct {
 Name string
 Age  int
}

//or
func someFunc(){
   	 arr := []struct{ UserId int }{{UserId: 200}}
      
     var m map[string]struct {
 	                   UserId string
     }
     fmt.Println("Hello ", arr, m)
}

8. Interface trong Go là gì, làm thế nào để triển khai một interface?

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.

Một kiểu dữ liệu (type struct) 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!"
 }

9. Những ảnh hưởng đến performance khi sử dụng các struct lớn làm tham số cho hàm trong Go là như thế nào? Làm thế nào để tối ưu hóa (optimize)?

Việc truyền các struct lớn theo giá trị (value) dẫn đến việc tạo ra một bản sao (copy), điều này có thể tốn kém về mặt bộ nhớ và hiệu suất. Để tối ưu hóa, nên truyền các con trỏ đến struct(*Struct) thay vì sao chép toàn bộ struct. Điều này giúp tránh việc nhân bản dữ liệu và giảm tải việc sao chép một lượng lớn bộ nhớ.

10. interface{} trong Go là gì, và nó liên quan đến empty interfaces như thế nào?

interface{} là một empty interface, có nghĩa là nó có thể chứa các giá trị của (any) bất kỳ kiểu nào vì mọi kiểu đều triển khai (implement) zero methods. Nó thường được sử dụng cho các cấu trúc dữ liệu generic hoặc khi làm việc với các function có thể chấp nhận bất kỳ kiểu nào (any). Sử dụng type assertions (value.(type)) và type switches cho phép bạn xác định dynamic type của một giá trị được lưu trữ trong interface{}.

11. Một số chiến lược để giảm tải GC overhead là gì?

  • Sử dụng object pooling để tái sử dụng bộ nhớ.
  • Giảm việc tạo ra các đối tượng có tuổi thọ ngắn.
  • Dự trữ trước bộ nhớ cho slices hoặc structs để tránh việc cấp phát thường xuyên.

12. Bạn sẽ implement việc xử lý lỗi và logging trong một dự án Go lớn như thế nào?

  • Đối với xử lý lỗi (error handling), sử dụng các custom error types để thêm ngữ cảnh vào lỗi, và bọc lỗi bằng fmt.Errorfđể có message lỗi với đầy đủ thông tin hơn.
  • Logging một cách có cấu trúc bằng cách sử dụng các thư viện như zap hoặc logrus giúp ghi lại log với chi tiết với các trường dữ liệu liên quan gây ra lỗi để dễ dàng tìm kiếm và phân tích về sau.
  • Sử dụng defer để đóng các tài nguyên và recover trong các phần quan trọng để xử lý các panics không mong muốn một cách gracefully.
  • Centralize error handling cho các thao tác thông thường như truy vấn cơ sở dữ liệu để đảm bảo tính nhất quán (consistency).

13. Làm thế nào để tránh các circular dependencies trong Go packages?

  • Refactor các chức năng chung (commonly shared) vào một package riêng (pkg) để tránh sự phụ thuộc chồng chéo.
  • Sử dụng interfaces để tách rời (decoupling) các dependencies, cho phép một package chỉ phụ thuộc vào interface thay vì implementation.
  • Tổ chức lại code để giảm số lượng dependencies giúp đảm bảo đồ thị phụ thuộc (dependency) không bị vòng lặp (circular dependencies).

14. Từ khóa defer là gì, và nó hoạt động như thế nào?

defer được sử dụng để hoãn việc thực thi một hàm cho đến khi surrounding function trả về.

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.How do you implement graceful shutdown of a Go HTTP server?

Answer: To implement a graceful shutdown, use a context with http.Server's Shutdown method to stop accepting new connections while allowing in-flight requests to complete. You can use a signal.Notify to listen for interrupt signals (e.g., SIGINT) and call Shutdown when the signal is received. This ensures the server shuts down cleanly without interrupting active requests.

srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
     log.Fatalf("Server error: %v", err)
 }
}()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
srv.Shutdown(context.Background())

16. Pointer trong Go là gì, và khai báo thế nào?

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).

x := 5
var p *int
p = &x // & sign generates a pointer to x, assign it to p
*p // get the value that pointer p pointing to

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) // len 0 cap 0 value []
s2 := make([]int, 3,5) // len 3 cap 5 value [0 0 0]
s3 := []int{} // len 0 cap 0 value []
var s4 []int = []int{} // len 0 cap 0 value []

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 và kiểm tra phần tử trong map?

m := make(map[string]int)
m2 := map[string]int{}
m["hi"]=1
if val,ok:=m["hello"];ok {
}

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)
 case <-time.After(time.Millisecond*500):
     fmt.Println("do something after a period amount of time")
 default:
     fmt.Println("No message received")
 }

24. Channel nil là gì?

Channel nil sẽ chặn cả việc gửi và nhận. Hữu dụng khi muốn disable một channel nào đó, nhưng cẩn trọng khi dùng vì có thể gây deadlock

func main() {
    var ch chan int // nil channel

    select {
    case ch <- 1: // This will block because ch is nil
        fmt.Println("Sent to channel")
    case <-time.After(1 * time.Second): // This ensures that after 1 second, it times out
        fmt.Println("Timed out")
    }
}

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). Thường dùng cho signaling, synchronization, hay dùng như 1 placeholder.

//{} can be used as values in a map when you only care about the existence of a key.
visited := map[string]struct{}{}
visited["example"] = struct{}{}

//Used to signal events without sending actual data.
done := make(chan struct{})
go func() {
    // Do some work...
    close(done) // Signal completion
}()

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 các lệnh go mod , go mod tidy, go clean -modcache 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).

  • go mod tidy: Dọn dẹp các file go.modgo.sum bằng cách loại bỏ các dependencies không sử dụng và thêm vào những cái còn thiếu mà module cần. Nó đảm bảo rằng các dependencies của module đồng bộ với mã thực tế.

  • go clean -modcache: Xóa nội dung của module cache, nơi lưu trữ các dependencies đã được tải về. Điều này hữu ích khi bạn muốn làm sạch cache để đảm bảo tải lại toàn bộ dependencies.

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 các constant sau đó tự động tăng thêm 1. (Thường dùng trong việc khai báo dữ liệu dạng Enum)

const (
	Type1 = iota + 5  // set iota to start with 5, so Type1 = 5
	Type2            // Type2 = 6
)

43. go test là gì?

go test được sử dụng để chạy các unit test được viết trong Go.

    go test -v
    go test -bench=. //test with Benchmark
    go test -cover  // analye test coverage
    go test -race   //test with race condition check

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ả)

Ngoài ra context còn được dùng để share data (ex: userID) giữa các functions mà không cần dùng đến các biến global varible.

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)

Trường hợp context được dùng để share data như sau

// Define a key type to avoid collisions in context
type contextKey string

const requestIDKey = contextKey("requestID")

// Function that accepts a context and retrieves the request ID
func processRequest(ctx context.Context) {
 requestID := ctx.Value(requestIDKey)
 fmt.Println("Processing request with ID:", requestID)
}

func main() {
 // Create a context with a value (e.g., request ID)
 ctx := context.WithValue(context.Background(), requestIDKey, "12345")

 // Pass the context with the request ID to another function
 processRequest(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). (thường dùng trong trường hợp cần thay đổi giá trị gốc hoặc giảm tải việc copy giá trị gốc)

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 tham chiếu đến cùng underlying array, hoặc array mới trong trường hợp việc append làm vượt quá capacity của underlying array.
  • 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]

Nói thêm về một vài loại copy trong golang

//Primitive Types (int, float, bool, etc.): Use direct assignment
a := 10
b := a // b is a copy of a

//Structs: Use direct assignment
type Person struct {
    Name string
    Age  int
}
p1 := Person{Name: "Alice", Age: 30}
p2 := p1 // p2 is a copy of p1

//Slices: Use the copy function
a := []int{1, 2, 3}
b := make([]int, len(a))
copy(b, a) // copies elements of a into b

//Maps: require a manual copy as assigning maps only copies the reference, not the contents.
original := map[string]int{"a": 1, "b": 2}
copyMap := make(map[string]int)
for k, v := range original {
    copyMap[k] = v
}

//Pointers:Assigning a pointer to another only copies the reference, not the underlying value. For a deep copy, you must manually create a new value and copy the data.

Cần phân biệt rõ Shallow Copy (chỉ copy top level data của struct và reference đến slice, map, pointer chứ không copy chúng) và Deep Copy (full copy nhưng phải thực hiện thủ công, hoặc dùng json.Marshal/Unmarshal hay external library như mitchellh/copystructure, deepcopier)

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.

Nếu bạn thấy bài viết này hữu ích, hãy cho mình biết bằng cách để lại 👍 hoặc bình luận!, hoặc nếu bạn nghĩ bài viết này có thể giúp ích cho ai đó, hãy thoải mái chia sẻ! Cảm ơn bạn rất nhiều! 😃


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í