Golang

1. What is a buffered channel?

A buffered channel has a specified capacity and allows sending of values until the buffer is full. It doesn't require a receiver to be ready to receive.

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
}

2. What is an interface in Go and how to implement it?

An interface in Go is a type that specifies a set of method signatures. It allows polymorphism by defining behavior.

A type implements an interface by implementing all its methods:

type Animal interface {
    Speak() string
 }

 type Dog struct{}

 func (d Dog) Speak() string {
     return "Woof!"
 }

3. What are the performance implications of using large struct types as function parameters in Go? How would you optimize it?

Passing large structs by value results in a copy being created, which can be expensive in terms of memory and performance. To optimize, pass pointers to structs (*Struct) instead of copying the entire struct. This avoids duplicating data and reduces the overhead of copying large amounts of memory.

4. What is interface{} in Go, and how does it relate to empty interfaces?

interface{} is an empty interface, meaning it can hold values of any type since all types implement zero methods. It is often used for generic data structures or when working with functions that can accept any type. Type assertions (value.(type)) and type switches allow you to determine the dynamic type of a value stored in an interface{}.

  1. What are some strategies to reduce GC overhead?

  • Using object pooling to reuse memory.

  • Reducing the allocation of short-lived objects.

  • Preallocating slices or structs to avoid frequent allocations.

Để giảm GC overhead (chi phí mà Garbage Collector phải bỏ ra để dọn dẹp bộ nhớ), ba chiến lược bạn nêu thực ra là các best practice thường dùng trong Golang hoặc các ngôn ngữ có GC khác.

Object Pooling (tái sử dụng bộ nhớ thay vì cấp phát mới)

  • Ý tưởng: thay vì mỗi lần cần object thì new hoặc make một cái mới (rồi để GC thu hồi), ta tái sử dụng object cũ đã được cấp phát.

  • Go cung cấp sẵn sync.Pool cho việc này:

    var bufPool = sync.Pool{
        New: func() any {
            return make([]byte, 1024) // pre-allocate 1KB buffer
        },
    }
    
    func handler() {
        buf := bufPool.Get().([]byte)
        defer bufPool.Put(buf)
    
        // use buf...
    }
  • Lợi ích: giảm số lần cấp phát heap → giảm tần suất GC phải quét.

  • Lưu ý: phù hợp cho object ngắn hạn, tái sử dụng nhiều (ví dụ buffer, struct request), nhưng không phải lúc nào cũng có lợi (pool sai có thể làm tốn RAM hơn).

Giảm allocation của short-lived objects

  • Trong Go, các object sống ngắn (tạo ra trong scope nhỏ rồi chết ngay) thường được cấp phát trên heap (GC phải thu gom) thay vì stack (compiler không escape được).

  • Ví dụ:

    // Tạo nhiều string tạm thời trong loop -> nhiều allocation
    for i := 0; i < 1000; i++ {
        s := fmt.Sprintf("value-%d", i) 
        process(s)
    }
  • Cách tối ưu:

    • Dùng strings.Builder hoặc bytes.Buffer thay vì cộng chuỗi hoặc fmt.Sprintf nhiều lần.

    • Hạn chế tạo slice/map tạm thời trong loop.

    • Viết code để compiler escape analysis có thể giữ dữ liệu trên stack thay vì heap.

Preallocating slices/structs

  • Nếu biết trước kích thước dữ liệu, nên preallocate thay vì để slice tự grow:

    // Sai: mỗi lần append có thể reallocate + copy
    arr := []int{}
    for i := 0; i < 1000; i++ {
        arr = append(arr, i)
    }
    
    // Đúng: cấp phát đủ capacity từ đầu
    arr := make([]int, 0, 1000)
    for i := 0; i < 1000; i++ {
        arr = append(arr, i)
    }
  • Tương tự cho map: nếu biết trước khoảng số phần tử → dùng make(map[int]int, expectedSize).

  • Lợi ích: giảm số lần reallocate, copy, giảm pressure cho GC.

5. How would you handle error handling and logging in a large Go project?

  • For error handling, use custom error types to add context to errors, and wrap errors using fmt.Errorf for better error messages.

  • Structured logging using libraries like zap or logrus helps capture detailed logs with fields for easier searching and analysis.

  • Use defer to close resources and recover in critical sections to handle unexpected panics gracefully.

  • Centralize error handling logic for common operations like database queries to ensure consistency.

6. How do you manage circular dependencies in Go packages?

  • Refactor common functionality into a separate package to avoid mutual dependency.

  • Use interfaces to decouple dependencies, allowing one package to depend only on the interface rather than the implementation.

  • Reorganize code to reduce the number of dependencies, ensuring that the dependency graph remains acyclic.

1. Refactor common functionality vào package riêng

  • Vấn đề thường gặp: package A cần gọi hàm trong package B, và ngược lại package B cũng gọi lại package A → thành vòng lặp.

  • Cách xử lý: tách phần code dùng chung ra package C để cả AB đều import C thay vì import nhau.

    // package util (tách riêng ra)
    package util
    func FormatName(s string) string { ... }
    
    // package user
    import "myapp/util"
    func CreateUser(name string) { util.FormatName(name) }
    
    // package order
    import "myapp/util"
    func CreateOrder(customer string) { util.FormatName(customer) }
  • Lợi ích: dependency graph rõ ràng, không bị vòng.

2. Dùng interfaces để decouple

  • Thay vì package A gọi trực tiếp struct cụ thể trong package B → hãy định nghĩa interface ở package A (consumer) và package B chỉ cần implement.

    // package user
    package user
    
    type Notifier interface {
        Send(msg string) error
    }
    
    type Service struct {
        notifier Notifier
    }
    
    func (s *Service) Signup() {
        s.notifier.Send("Welcome new user")
    }
    
    // package email
    package email
    type EmailService struct {}
    func (e *EmailService) Send(msg string) error { ... }
  • Giờ user không cần import email, chỉ biết Notifier.

  • main sẽ wire chúng lại:

    notifier := &email.EmailService{}
    userSvc := user.Service{notifier: notifier}

→ Đây chính là inversion of dependency, giữ cho dependency graph 1 chiều.

3. Reorganize code để dependency graph luôn acyclic

  • Khi design, hãy nhớ Go compiler enforce acyclic import graph → nếu vòng thì sẽ fail ngay.

  • Nguyên tắc tổ chức package:

    • Theo layer: domain → usecase/service → delivery (http/grpc) → main (composition root).

    • Package dưới (domain, entity) không được import package trên (API, service).

    • Ví dụ trong Clean Architecture (DDD):

      /domain       # entities, interfaces (no deps)
      /repository   # implement domain.Repository
      /service      # business logic, depends on domain
      /delivery     # HTTP/GRPC handlers, depend on service
      /cmd          # main.go, wires all together
  • Như vậy không bao giờ có vòng.

7. What is a pointer in Go, how to declare it?

A pointer holds the memory address of a value. It is used to pass references instead of 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

8 .What is a nil channel?

A nil channel blocks both sending and receiving operations. But becareful, It might cause deadlock.

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")
    }
}

9. What is the init function?

init is a special function that initializes package-level variables. It is executed before main.

10. Can you have multiple init functions?

Yes, but they will be executed in the order they appear.

Last updated