Việc làm

18/09 · Hồ Chí Minh · Tại văn phòng

Golang Backend Engineer

FireGroup · Backend Developer

Xem mô tả & ứng tuyển trên ITviecMô tả công việc, lương và ứng tuyển nằm ở ITviec.

10 câu hỏi có thể gặp khi phỏng vấn

Chọn theo kỹ năng và cấp của tin (Golang, SQL & Database, Data Engineer & Data Analyst). Gợi ý trả lời do Drama Công Ty biên soạn để bạn ôn nhanh, không phải đề của công ty.

  1. 01Cần xử lý 10.000 job (gọi API ngoài) với tối đa 20 job chạy cùng lúc, dừng sớm nếu có lỗi. Bạn thiết kế thế nào?Golang · Senior

    Gợi ý trả lời

    Đây là bài worker pool có giới hạn concurrency. Cách gọn nhất là errgroup (package golang.org/x/sync/errgroup):

    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(20)
    for _, job := range jobs {
        g.Go(func() error { return process(ctx, job) })
    }
    if err := g.Wait(); err != nil { return err }
    
    • SetLimit khiến g.Go chặn lại khi đã đủ 20 goroutine đang chạy.
    • Lỗi đầu tiên sẽ huỷ ctx; các job khác phải tôn trọng ctx (truyền vào HTTP request) để dừng sớm.

    Cách cổ điển: tạo 20 worker đọc từ một channel job, gửi kết quả qua channel khác, đóng channel đúng thứ tự.

    Cần nghĩ thêm: rate limit phía API ngoài (golang.org/x/time/rate), retry có backoff, có cần dừng hết khi một job lỗi hay chỉ ghi lại lỗi rồi tiếp tục.

    Hay sai: tạo 10.000 goroutine cùng lúc gọi API ngoài, làm cạn connection và bị đối tác chặn.

    Ôn thêm câu hỏi Golang →
  2. 02Goroutine khác gì OS thread? Làm sao chờ nhiều goroutine chạy xong?Golang · Junior

    Gợi ý trả lời

    Goroutine là "luồng nhẹ" do runtime Go quản lý: stack khởi đầu chỉ vài KB và tự giãn, chi phí tạo rất thấp. Scheduler của Go phân phối nhiều goroutine lên một số ít OS thread (mô hình M:N), số thread chạy song song tối đa theo GOMAXPROCS.

    Chờ nhiều goroutine bằng sync.WaitGroup:

    var wg sync.WaitGroup
    for _, u := range urls {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fetch(u)
        }()
    }
    wg.Wait()
    

    Go 1.25 có thêm wg.Go(func() { ... }) gói gọn Add và Done.

    Hay sai: gọi wg.Add(1) bên trong goroutine, khiến Wait có thể trả về trước khi goroutine kịp đăng ký. Một lỗi khác: hàm main kết thúc thì mọi goroutine bị dừng theo, không ai chờ chúng.

    Ôn thêm câu hỏi Golang →
  3. 03Array và slice khác nhau thế nào? Vì sao append đôi khi làm thay đổi cả slice khác?Golang · Junior

    Gợi ý trả lời

    Array có độ dài cố định và là giá trị (gán là copy toàn bộ). Slice là một "khung nhìn" gồm con trỏ đến mảng bên dưới, len và cap.

    • Gán hoặc cắt slice (b := a[1:3]) không copy dữ liệu; hai slice dùng chung mảng bên dưới.
    • append khi còn cap sẽ ghi thẳng vào mảng chung, nên có thể ghi đè dữ liệu slice khác đang thấy. Khi hết cap, nó cấp phát mảng mới và từ đó hai slice tách rời.
    a := []int{1, 2, 3, 4}
    b := a[:2]
    b = append(b, 99) // a thành [1 2 99 4]
    
    • Muốn tách hẳn: slices.Clone(a) hoặc dùng full slice expression a[:2:2] để giới hạn cap.
    • Biết trước kích thước thì make([]T, 0, n) để tránh cấp phát lại nhiều lần.

    Người phỏng vấn muốn nghe: luôn dùng giá trị trả về của append (s = append(s, x)).

    Ôn thêm câu hỏi Golang →
  4. 04Phân biệt INNER JOIN, LEFT JOIN, RIGHT JOIN và FULL OUTER JOIN. Khi nào dùng LEFT JOIN?SQL & Database · Junior

    Gợi ý trả lời

    JOIN quyết định giữ lại những dòng nào khi ghép hai bảng theo điều kiện.

    • INNER JOIN: chỉ giữ dòng khớp ở cả hai bảng.
    • LEFT JOIN: giữ mọi dòng bảng trái; bảng phải không khớp thì cột thành NULL.
    • RIGHT JOIN: ngược lại với LEFT, ít dùng vì có thể đổi chỗ bảng.
    • FULL OUTER JOIN: giữ tất cả dòng của cả hai (MySQL không hỗ trợ trực tiếp, phải UNION hai LEFT JOIN).

    Ví dụ tìm khách chưa từng đặt hàng:

    SELECT c.id, c.name
    FROM customers c
    LEFT JOIN orders o ON o.customer_id = c.id
    WHERE o.id IS NULL;
    

    Hay sai: đặt điều kiện lọc bảng phải trong WHERE (vd. WHERE o.status = 'paid') làm LEFT JOIN biến thành INNER JOIN; điều kiện đó phải để trong ON.

    Ôn thêm câu hỏi SQL & Database →
  5. 05WHERE và HAVING khác nhau thế nào? Thứ tự thực thi logic của một câu SELECT ra sao?SQL & Database · Junior

    Gợi ý trả lời

    WHERE lọc từng dòng trước khi gom nhóm; HAVING lọc sau GROUP BY, nên dùng được hàm tổng hợp như COUNT, SUM.

    Thứ tự logic: FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT/OFFSET.

    SELECT department_id, AVG(salary) AS avg_salary
    FROM employees
    WHERE status = 'active'
    GROUP BY department_id
    HAVING AVG(salary) > 20000000;
    
    • Vì SELECT chạy sau WHERE, bạn không dùng được alias của SELECT trong WHERE (đa số DB).
    • Điều kiện không cần hàm tổng hợp thì nên để ở WHERE để giảm số dòng phải gom nhóm.

    Người phỏng vấn muốn nghe: bạn hiểu thứ tự logic, từ đó giải thích được vì sao alias hoặc aggregate dùng sai chỗ bị lỗi.

    Ôn thêm câu hỏi SQL & Database →
  6. 06WHERE khác HAVING thế nào? Thứ tự thực thi logic của một câu SELECT là gì?Data Engineer & Data Analyst · Junior

    Gợi ý trả lời

    WHERE lọc từng dòng trước khi gộp nhóm; HAVING lọc nhóm sau khi đã GROUP BY và tính hàm tổng hợp.

    Thứ tự logic: FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT.

    SELECT customer_id, COUNT(*) AS orders
    FROM orders
    WHERE created_at >= DATE '2026-01-01'
    GROUP BY customer_id
    HAVING COUNT(*) >= 3;
    

    Hay sai: dùng alias định nghĩa trong SELECT ngay ở WHERE (đa số hệ quản trị báo lỗi), hoặc lọc điều kiện dòng bằng HAVING làm truy vấn chậm hơn.

    Ôn thêm câu hỏi Data Engineer & Data Analyst →
  7. 07Go xử lý lỗi như thế nào? Khi nào dùng panic? errors.Is và errors.As để làm gì?Golang · Junior

    Gợi ý trả lời

    Lỗi trong Go là giá trị kiểu error, được trả về như kết quả thông thường và phải kiểm tra tường minh. panic chỉ dành cho tình huống không thể tiếp tục hoặc lỗi lập trình (ví dụ cấu hình bắt buộc bị thiếu lúc khởi động), không dùng cho lỗi nghiệp vụ.

    Bọc lỗi để giữ ngữ cảnh mà không mất lỗi gốc:

    if err != nil {
        return fmt.Errorf("load user %d: %w", id, err)
    }
    // Ở tầng trên:
    if errors.Is(err, sql.ErrNoRows) { /* trả 404 */ }
    
    • errors.Is: so với một lỗi cụ thể trong chuỗi bọc.
    • errors.As: lấy ra lỗi thuộc một kiểu cụ thể để đọc field của nó.
    • recover trong defer dùng ở biên (middleware HTTP) để một request panic không làm sập cả server.

    Hay sai: dùng %v thay %w khi bọc, làm errors.Is không tìm thấy lỗi gốc.

    Ôn thêm câu hỏi Golang →
  8. 08Interface trong Go khác gì Java hay C#? Vì sao một interface chứa con trỏ nil lại khác nil?Golang · Junior

    Gợi ý trả lời

    Go implement interface ngầm định: kiểu nào có đủ method thì tự thoả mãn interface, không cần khai báo implements. Nhờ vậy interface thường được định nghĩa ở phía sử dụng, nhỏ gọn (io.Reader chỉ có một method), và dễ thay bằng mock khi test.

    Giá trị interface gồm hai phần: kiểu và giá trị. Nó chỉ bằng nil khi cả hai đều nil.

    func find() error {
        var err *MyError = nil
        return err // interface có kiểu *MyError, nên != nil
    }
    

    Caller kiểm tra err != nil sẽ thấy true dù "không có lỗi".

    Hay sai: khai báo interface lớn ở phía cài đặt cho mọi struct, theo thói quen từ Java. Câu hay được nhắc trong cộng đồng Go: nhận vào interface, trả về struct cụ thể. Khi không có lỗi, hãy return nil trực tiếp.

    Ôn thêm câu hỏi Golang →
  9. 09Channel buffered và unbuffered khác nhau thế nào? select dùng để làm gì? Khi nào xảy ra deadlock?Golang

    Gợi ý trả lời

    • Unbuffered: gửi bị chặn đến khi có bên nhận (và ngược lại). Đây là điểm đồng bộ giữa hai goroutine.
    • Buffered (make(chan T, n)): gửi chỉ bị chặn khi buffer đầy, nhận bị chặn khi buffer rỗng.

    select chờ trên nhiều channel cùng lúc, thường dùng cho timeout hoặc huỷ:

    select {
    case res := <-results:
        handle(res)
    case <-ctx.Done():
        return ctx.Err()
    }
    

    Quy tắc: chỉ bên gửi đóng channel; gửi vào channel đã đóng gây panic; nhận từ channel đã đóng trả giá trị zero ngay lập tức (v, ok := <-ch để phân biệt). for range ch dừng khi channel đóng.

    Deadlock xảy ra khi mọi goroutine đều đang chờ nhau, ví dụ gửi vào channel unbuffered mà không có ai nhận. Runtime báo "all goroutines are asleep" nếu toàn bộ chương trình bị kẹt; kẹt một phần thì không báo gì và thành goroutine leak.

    Ôn thêm câu hỏi Golang →
  10. 10Bạn thấy mình ở đâu sau 2–3 năm nữa?Câu hỏi hành vi & văn hoá

    Gợi ý trả lời

    Người phỏng vấn muốn biết mục tiêu của bạn có khớp với hướng phát triển ở công ty và bạn có định gắn bó không.

    • Nêu hướng phát triển cụ thể, thực tế: đi sâu chuyên môn (senior, chuyên gia một mảng) hay quản lý (lead, PM).
    • Kèm kế hoạch: kỹ năng cần học, trách nhiệm muốn nhận thêm.
    • Gắn với cơ hội ở vị trí đang ứng tuyển.

    Câu mẫu: "Trong 2–3 năm tới em muốn trở thành senior có thể tự thiết kế và chịu trách nhiệm một module lớn, đồng thời hướng dẫn được các bạn mới. Em đang học thêm về thiết kế hệ thống và thấy team bên mình có nhiều bài toán ở quy mô lớn, rất hợp để em phát triển theo hướng đó."

    Hay sai: "Em muốn mở công ty riêng" hoặc "em chưa nghĩ tới". Cũng đừng nói muốn ngồi vào chỗ của người đang phỏng vấn.

    Ôn thêm câu hỏi Câu hỏi hành vi & văn hoá →

Bạn đã phỏng vấn ở FireGroup?

Kể lại các vòng và câu hỏi đã gặp (ẩn danh) để người sau ôn đúng trọng tâm.

Chia sẻ kinh nghiệm phỏng vấn