Việc làm

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

Senior Fullstack Engineer

Nami Technology Joint Stock Company · Fullstack 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, C# & .NET, DevOps & CI/CD). 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. 02Channel 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 →
  3. 03Map trong Go có an toàn khi dùng đồng thời không? Bạn phát hiện và xử lý race condition thế nào?Golang

    Gợi ý trả lời

    Không. Đọc và ghi map đồng thời có thể làm runtime dừng chương trình với lỗi "concurrent map read and map write". Các biến dùng chung khác cũng có data race nhưng thường im lặng cho kết quả sai.

    Phát hiện bằng race detector: go test -race ./... hoặc go run -race. Nên bật trong CI.

    Cách xử lý:

    • sync.Mutex bọc map, hoặc sync.RWMutex khi đọc nhiều hơn ghi rất nhiều.
    • sync.Map hợp với vài trường hợp đặc thù (key ghi một lần đọc nhiều lần, hoặc mỗi goroutine dùng tập key riêng); không phải thay thế mặc định.
    • Bộ đếm đơn giản: atomic.Int64.
    • Hoặc tránh chia sẻ: để một goroutine sở hữu dữ liệu, các goroutine khác gửi yêu cầu qua channel.

    Hay sai: copy struct có chứa sync.Mutex (truyền theo giá trị), khiến mỗi bản có một khoá riêng. go vet cảnh báo được lỗi này.

    Ôn thêm câu hỏi Golang →
  4. 04Dependency Injection trong ASP.NET Core: phân biệt Transient, Scoped và Singleton.C# & .NET

    Gợi ý trả lời

    ASP.NET Core có DI container sẵn; lifetime quyết định khi nào tạo instance mới.

    • Transient: mỗi lần resolve tạo mới. Hợp với service nhẹ, không trạng thái.
    • Scoped: một instance cho mỗi request (mỗi scope). DbContext mặc định là scoped.
    • Singleton: một instance cho cả vòng đời ứng dụng. Phải thread-safe.
    builder.Services.AddScoped<IOrderService, OrderService>();
    builder.Services.AddSingleton<IClock, SystemClock>();
    

    Hay sai: inject service scoped (như DbContext) vào singleton, gọi là captive dependency: DbContext bị giữ mãi và dùng chung giữa nhiều thread. Môi trường Development sẽ báo lỗi khi validate scope. Nếu singleton cần dữ liệu scoped, hãy inject IServiceScopeFactory và tự tạo scope.

    Ôn thêm câu hỏi C# & .NET →
  5. 05Middleware trong ASP.NET Core là gì? Thứ tự đăng ký có quan trọng không?C# & .NET

    Gợi ý trả lời

    Middleware là các bước nối thành pipeline xử lý request: mỗi bước có thể xử lý, gọi bước kế tiếp, rồi xử lý tiếp response trên đường về. Thứ tự rất quan trọng vì request đi qua đúng theo thứ tự đăng ký.

    app.UseExceptionHandler("/error");
    app.UseHttpsRedirection();
    app.UseRouting();
    app.UseCors();
    app.UseAuthentication();
    app.UseAuthorization();
    app.MapControllers();
    
    • Exception handler phải đứng đầu để bắt lỗi của mọi bước sau.
    • UseAuthentication phải trước UseAuthorization, nếu không user luôn là ẩn danh.
    • Middleware có thể "short-circuit", vd. trả cache hoặc 401 mà không gọi next.

    Người phỏng vấn muốn nghe: bạn từng viết middleware riêng, vd. gắn correlation id hoặc log thời gian xử lý, và phân biệt được middleware với filter của MVC.

    Ôn thêm câu hỏi C# & .NET →
  6. 06Liveness, readiness và startup probe khác nhau thế nào? Cấu hình sai thì hậu quả gì?DevOps & CI/CD

    Gợi ý trả lời

    Readiness quyết định Pod có nhận traffic không; liveness quyết định có restart container không; startup probe tạm hoãn hai probe kia cho tới khi app khởi động xong.

    • Readiness fail: Pod bị gỡ khỏi endpoint của Service, không bị restart.
    • Liveness fail: kubelet restart container.
    • Startup probe hợp với app khởi động chậm (JVM, nạp model).

    Lỗi hay gặp:

    • Liveness kiểm tra cả DB bên ngoài: DB chậm là toàn bộ Pod bị restart dây chuyền.
    • Không có readiness: rolling update đẩy traffic vào Pod chưa sẵn sàng, gây lỗi 5xx.

    Người phỏng vấn muốn nghe: liveness chỉ nên kiểm tra chính process còn sống, phần phụ thuộc để cho readiness.

    Ôn thêm câu hỏi DevOps & CI/CD →
  7. 07context.Context dùng để làm gì? Có những quy ước nào khi dùng context?Golang

    Gợi ý trả lời

    Context mang tín hiệu huỷ, deadline và một ít giá trị gắn với request, truyền xuyên suốt chuỗi gọi hàm. Khi client ngắt kết nối hoặc hết thời gian, mọi thao tác bên dưới (truy vấn DB, gọi HTTP) đều có thể dừng sớm.

    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()
    row := db.QueryRowContext(ctx, query, id)
    

    Quy ước:

    • Context là tham số đầu tiên, tên ctx; không lưu context trong struct.
    • Luôn defer cancel() để giải phóng tài nguyên.
    • Goroutine chạy lâu phải kiểm tra ctx.Done().
    • WithValue chỉ dùng cho dữ liệu theo request (request ID, thông tin trace), không dùng để truyền tham số tuỳ chọn.

    Hay sai: dùng context của request cho việc chạy nền sau khi đã trả response. Request xong là context bị huỷ; hãy dùng context.WithoutCancel hoặc context riêng.

    Ôn thêm câu hỏi Golang →
  8. 08defer hoạt động thế nào? Vòng lặp for kết hợp goroutine và closure có thay đổi gì từ Go 1.22?Golang

    Gợi ý trả lời

    defer hoãn lời gọi hàm đến khi hàm bao ngoài kết thúc (kể cả khi panic), chạy theo thứ tự LIFO. Tham số của lời gọi được tính ngay lúc gặp defer, không phải lúc chạy.

    for i := 0; i < 3; i++ {
        defer fmt.Println(i) // in 2, 1, 0
    }
    
    • Đừng defer f.Close() trong vòng lặp mở hàng nghìn file: chúng chỉ đóng khi hàm kết thúc. Tách thân vòng lặp thành hàm riêng.

    Về vòng lặp: trước Go 1.22, biến vòng lặp dùng chung cho mọi lần lặp nên goroutine hay closure thường thấy giá trị cuối cùng. Từ Go 1.22, mỗi lần lặp có biến riêng (khi go.mod khai báo 1.22 trở lên), nên lỗi này không còn.

    Người phỏng vấn muốn nghe: bạn biết thay đổi này phụ thuộc phiên bản trong go.mod.

    Ôn thêm câu hỏi Golang →
  9. 09Goroutine leak là gì? Làm sao phát hiện và phòng tránh?Golang · Senior

    Gợi ý trả lời

    Goroutine leak là goroutine bị chặn mãi mãi và không bao giờ kết thúc, giữ theo bộ nhớ và tài nguyên nó tham chiếu. Số goroutine tăng dần theo thời gian là dấu hiệu rõ nhất.

    Nguyên nhân hay gặp:

    • Gửi vào channel mà bên nhận đã bỏ đi (ví dụ đã trả về vì timeout).
    • Nhận từ channel không bao giờ được gửi hoặc đóng.
    • Vòng lặp chạy nền không kiểm tra ctx.Done().
    • HTTP response không đóng Body.
    ch := make(chan Result, 1) // buffer 1: goroutine gửi xong vẫn thoát được
    go func() { ch <- slowCall() }()
    select {
    case r := <-ch: return r, nil
    case <-ctx.Done(): return Result{}, ctx.Err()
    }
    

    Phát hiện: theo dõi runtime.NumGoroutine(), xem profile goroutine qua net/http/pprof, dùng thư viện goleak trong test.

    Người phỏng vấn muốn nghe: mỗi goroutine bạn tạo phải có câu trả lời rõ ràng cho câu hỏi "nó kết thúc khi nào?".

    Ôn thêm câu hỏi Golang →
  10. 10Vì sao bạn muốn rời công ty hiện tại?Câu hỏi hành vi & văn hoá

    Gợi ý trả lời

    Trả lời thật nhưng hướng về phía trước: nói điều bạn đang tìm kiếm, không kể xấu nơi cũ.

    Lý do chấp nhận được: muốn thử thách mới, muốn làm sản phẩm, dự án kết thúc, công ty tái cấu trúc, thu nhập chưa tương xứng (nói khéo).

    Câu mẫu: "Em đã làm 2 năm ở dự án outsourcing, học được nhiều về quy trình và làm việc với khách nước ngoài. Giờ em muốn gắn bó lâu dài với một sản phẩm để thấy được tác động của những gì mình làm, và vị trí bên mình đúng hướng đó."

    Hay sai: chê sếp, chê đồng nghiệp, kể chuyện drama. Người phỏng vấn sẽ nghĩ bạn cũng sẽ nói về công ty họ như vậy. Nếu nhảy việc nhiều, chuẩn bị lời giải thích cho từng lần.

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

Bạn đã phỏng vấn ở Nami Technology Joint Stock Company?

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