Viết HTTP server bằng thư viện chuẩn của Go: không cần framework

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Trong nhiều ngôn ngữ, bạn gần như phải chọn một framework trước khi viết được API. Với Go thì khác: thư viện chuẩn net/http đủ mạnh để chạy dịch vụ thật. Hiểu nó trước sẽ giúp bạn dùng bất kỳ framework nào sau này một cách tự tin.

Server nhỏ nhất

package mainimport (    "fmt"    "log"    "net/http")func main() {    http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {        fmt.Fprintln(w, "Xin chào từ Go")    })    log.Fatal(http.ListenAndServe(":8080", nil))}

Một handler là hàm nhận ResponseWriter (để ghi phản hồi) và *Request (thông tin yêu cầu). Chạy go run . và truy cập localhost:8080/hello. Mỗi request được xử lý trong một goroutine riêng, nên server tự phục vụ nhiều kết nối đồng thời.

Định tuyến theo phương thức và tham số

Từ Go 1.22, bộ định tuyến chuẩn hỗ trợ ghi phương thức và tham số đường dẫn ngay trong mẫu:

mux := http.NewServeMux()mux.HandleFunc("GET /books/{id}", func(w http.ResponseWriter, r *http.Request) {    id := r.PathValue("id")    fmt.Fprintf(w, "sách số %s", id)})mux.HandleFunc("POST /books", createBook)

Với phiên bản Go cũ hơn, bạn cần tự kiểm tra r.Method hoặc dùng thư viện định tuyến bên thứ ba.

Trả JSON và đọc JSON

type Book struct {    ID    int    `json:"id"`    Title string `json:"title"`}func createBook(w http.ResponseWriter, r *http.Request) {    var in Book    if err := json.NewDecoder(r.Body).Decode(&in); err != nil {        http.Error(w, "JSON không hợp lệ", http.StatusBadRequest)        return    }    if in.Title == "" {        http.Error(w, "thiếu tiêu đề", http.StatusUnprocessableEntity)        return    }    in.ID = 1    w.Header().Set("Content-Type", "application/json")    w.WriteHeader(http.StatusCreated)    json.NewEncoder(w).Encode(in)}

Các thẻ trong dấu huyền (backtick) như json:"title" quy định tên trường trong JSON. Nhớ đặt header và mã trạng thái trước khi ghi phần thân, vì sau khi bắt đầu ghi body, bạn không đổi được header nữa.

Middleware kiểu Go

Middleware trong Go chỉ là hàm bọc một handler:

func logging(next http.Handler) http.Handler {    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {        start := time.Now()        next.ServeHTTP(w, r)        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))    })}srv := &http.Server{    Addr:         ":8080",    Handler:      logging(mux),    ReadTimeout:  5 * time.Second,    WriteTimeout: 10 * time.Second,}log.Fatal(srv.ListenAndServe())

Lưu ý cấu hình timeout cho http.Server. Hàm tiện lợi http.ListenAndServe không đặt timeout, khiến server dễ bị chiếm kết nối bởi client chậm.

Tắt server nhẹ nhàng

Khi nhận tín hiệu dừng (như lúc triển khai bản mới), hãy cho các request đang xử lý hoàn tất thay vì cắt ngang:

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)defer stop()go srv.ListenAndServe()<-ctx.Done()shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)defer cancel()srv.Shutdown(shutdownCtx)

Khi nào dùng thêm framework?

Thư viện chuẩn thiếu vài tiện ích như nhóm route phức tạp, binding và validate tự động. Các thư viện như chi, gin hay echo bổ sung những thứ đó. Nhiều đội Go vẫn ưu tiên giữ gần thư viện chuẩn vì ít phụ thuộc, dễ nâng cấp và dễ hiểu. Lời khuyên là bắt đầu với net/http, khi nào thấy đau rồi mới thêm công cụ.

Đóng gói và triển khai

Một điểm hấp dẫn của Go là biên dịch ra một tệp nhị phân duy nhất, không cần cài runtime trên máy chủ. Bạn có thể đóng gói vào một image Docker rất nhỏ bằng build nhiều giai đoạn: giai đoạn đầu biên dịch, giai đoạn sau chỉ chép tệp nhị phân. Điều này giúp triển khai nhanh, đơn giản và giảm bề mặt tấn công.

Kiểm thử handler với httptest

Một ưu điểm lớn của việc dùng thư viện chuẩn là kiểm thử rất tự nhiên. Gói httptest cho phép giả lập request và ghi lại phản hồi mà không cần mở cổng mạng:

func TestCreateBook(t *testing.T) {    body := strings.NewReader(`{"title":"Go cơ bản"}`)    req := httptest.NewRequest(http.MethodPost, "/books", body)    rec := httptest.NewRecorder()    createBook(rec, req)    if rec.Code != http.StatusCreated {        t.Fatalf("mong đợi 201, nhận %d", rec.Code)    }}

Test chạy trong vài mili giây và không phụ thuộc môi trường. Hãy viết thêm test cho các nhánh lỗi như JSON sai hoặc thiếu trường, vì đó là nơi lỗi hay ẩn nấp. Thói quen này đi rất tốt cùng triết lý đơn giản của Go: ít thành phần, dễ hiểu, dễ kiểm chứng.

Lời khuyên cấu trúc dự án Go

Khi dự án lớn hơn, đừng dồn mọi thứ vào main.go. Một cách tổ chức phổ biến là tách thư mục cmd chứa điểm khởi chạy, internal chứa code riêng của ứng dụng, chia nhỏ theo miền nghiệp vụ như books, users. Mỗi gói có handler, logic và truy cập dữ liệu rõ ràng, phụ thuộc đi theo một chiều. Tránh đặt tên gói chung chung như utils hay common, vì chúng dần biến thành nơi chứa mọi thứ không biết đặt đâu.

Kết luận

Với vài chục dòng, bạn đã có server có JSON, middleware, timeout và tắt máy lịch sự. Hãy thử xây một API quản lý sách nhỏ và viết test cho handler bằng gói net/http/httptest, bạn sẽ thấy Go đơn giản mà mạnh đến mức nào.

Khi cần một lộ trình học có hệ thống, bạn có thể tham khảo khóa học trên codebem.com.

author_photo
Nguyễn Đình Cường

0 comment