hi tìm hiểu về ngôn ngữ lập trình Go, lời khuyên mà người mới học thường gặp nhất là thay vì tìm một web framework có sẵn thì hãy tìm hiểu và sử dụng package net/http đi kèm sẵn của Go. Có thể thấy, net/http thật sự rất mạnh, đủ dùng cho phần lớn các trường hợp phát triển web thông thường. Ở bài viết này, chúng ta cũng tìm hiểm các tính năng thông dụng nhất của net/http

Cơ bản về net/http

HTTP server đơn giản

Đầu tiên, sử dụng net/http chúng ta hãy khỏi tạo một server HTTP vô cùng đơn giản

// main.go
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", HelloServer)
    err := http.ListenAndServe(":8080", nil)
    
    if err != nil {
        log.Fatalf("Error creating server %s\n", err.Error())
    }
}

func HelloServer(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:])
}

Ở đoạn code trên, chúng ta tạo một http server ở cổng 8080 và dùng hàm http.HandleFunc để thiết lập thực thi chạy hàm HelloServer mỗi khi có request đến đường dẫn root /. Khi có request gửi đến, HelloServer sẽ được gọi và hai paramerter w(http.ResponseWriter) và r(*http.Request) sẽ được set tương ứng là 2 giá trị đại diện cho response và request của request đó.
Ta sử dụng câu lệnh sau để chạy test thử http server:

go run main.go

Truy cập vào địa chỉ web http://localhost:8080/handsome/good/boy, ta sẽ thấy đoạn text sau được trả về

Hello, handsome/good/guy!

Để add thêm các tính năng cho từng đường dẫn, ta đơn giản chỉ cần tiếp tục sử dụng hàm http.HandleFunc:

http.handleFunc("/route1", Function1)
http.handleFunc("/route2", Function2)

HTTP Client

Ta cũng có thể sử dụng net/http để gửi request HTTP. Sửa đoạn code ở trên để thêm 1 đường dẫn để gọi API thông tin về mèo như sau

func main() {
// ...
    http.HandleFunc("/api/facts", CatFacts)
// ...
}

// ...
func GetFacts(w http.ResponseWriter, r *http.Request) {
    resp, err := http.Get("https://cat-fact.herokuapp.com/facts/random?animal_type=cat&amount=29")
    if err != nil {
        log.Printf("Error getting data %s", err.Error())
    }

    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Printf("Error parsing data %s", err.Error())
    }

    var catFacts []CatFact
    err = json.Unmarshal(body, &catFacts)

    if err != nil {
        log.Printf("Error parsing json %s", err.Error())
    }

    w.Header().Set("Content-Type", "application/json")
    err = json.NewEncoder(w).Encode(catFacts)
    if err != nil {
        log.Printf("Error encoding json %s", err.Error())
    }
}

Truy cập vào địa chỉ http://localhost:8080/api/cat, ta sẽ thấy chuỗi json kết quả được trả về (hơi bị vô nghĩa khi gọi API lấy json rồi parse lại thành Go Struct rồi lại trả về json, nhưng vì là code demo nên tất cả đều là ok :D). Ở đoạn code trên ta sử dụng hàm http.Get để gửi request lấy dữ liệu từ api cat-fact. Response trả về là định dạng io.Reader do đó phải dùng hàm ioutil.ReadAll để chuyển sang dạng []byte rồi parse thành dạng Struct CatFact. Để trả về data dưới định dạng json thì ta cần set Header Content-Type cho response trả về đồng thời ghi dữ liệu muốn trả về vào http.ResponseWriter w.

w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(catFacts)

net/http hiện giờ chỉ hỗ trợ GET, POST và HEAD tương ứng với các hàm http.Get, http.Post, http.Post. Ngoài ra, để có thể chỉnh sửa các setting cho request ta có thể sử dụng 2 cách tạo request khác:

  • Thay vì dùng trực tiếp các method từ http, khởi tạo một Client object và gọi các method Get, Post, Head
    var client = &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Get("...")
    
  • Dùng hàm http.NewRequest
    var client = &http.Client{}
    req, err := http.NewRequest("GET", "...", nil)
    req.Header.Add("If-None-Match", `W/"wyzzy"`)
    resp, err := client.Do(req)
    

ServerMux

Hầu hết các web framework đều có một thành phần quan trọng là Router để giúp người phát triển có thể thiết kế các đường dẫn trong ứng dụng của mình, mapping các đường dẫn tới các hàm thực thi trong ứng dụng. Có rất nhiều các http router được viết và opensource cho go, net/httpđã đi kèm sẵn với ServerMux, một HTTP request multiplexer, đủ dùng cho việc làm router đơn giản. Việc sử dụng http.HandleFunc cùng với http.ListenAndServe như ở dưới đây thực chất là đã định nghĩa mapping đường dẫn cho DefaultServerMux, một ServerMux mặc định.

http.HandleFunc("/", HelloServer)
err := http.ListenAndServe(":8080", nil)

Tuy nhiên đây là ServerMux global có thể được access trong toàn bộ ứng dụng nên có thể xảy ra các vấn đề về security nên tốt nhất ta nên tạo riêng 1 ServerMuxở local scope và dùng riêng.

//...
mux := http.NewServeMux()
mux.HandleFunc("/", HelloServer)
mux.HandleFunc("/api/facts", GetFacts)
err := http.ListenAndServe(":8080", mux)
//...

Vì bản chất của ServerMux chỉ là một HTTP request multiplexer nên tính năng của ServerMux khá là hạn chế. Với các đường dẫn kết thúc bằng / ví dụ /home/ thì ServerMux sẽ match tất cả những đường dẫn con như là /home/sweet hay /home/sweet/home. Do vậy với http server ở trên của chúng ta, khi truy cập vào /home/sweet/home thì hàm HelloServer cũng được gọi. Còn nếu đường dẫn không kết thúc bằng / thì chỉ khi nào đường dẫn match đúng thì hàm handler mới được gọi. Ngoài ra, ServerMux cũng không support wildcat matching, matching theo method của Request, tự động lấy parameter từ đường dẫn. Nếu muốn làm những việc trên thì đều phải code bằng tay. Ví dụ như sau:

  • Giới hạn chỉ xử lý GET request
    func HelloServer(w http.ResponseWriter, r *http.Request) {
        if r.Method != "GET" {
            w.Header().Set("Allow", "GET")
            http.Error(w, "Method Not Allowed", 405)
            return
        }
        fmt.Fprintf(w, "Hello, %s!", r.URL.Path[1:]
    }
    
  • Đọc query parameter từ đường dẫn
    //...
    func GetFacts(w http.ResponseWriter, r *http.Request) {
        if r.Method != "GET" {
            w.Header().Set("Allow", "GET")
            http.Error(w, "Method Not Allowed", 405)
            return
        }
    
        animalType := r.URL.Query().Get("animal_type")
        if animalType == "" {
            animalType = "cat"
        }
    
        amount, err := strconv.Atoi(r.URL.Query().Get("amount"))
        if err != nil {
          amount = 29
        }
    
        resp, err := http.Get(fmt.Sprintf("https://cat-fact.herokuapp.com/facts/random?animal_type=%s&amount=%d", animalType, amount))
          //...
    }
    

Có thể thấy ta nên chỉ dùng ServeMux cho những ứng dụng web đơn giản với số đường dẫn ít, với những ứng dụng đòi hỏi đường dẫn phức tạp hay REST API thì ta nên sử dụng một trong rất nhiều những router bên thứ ba với đầy đủ tính năng hơn.

Kết luận

Bài viết này đã giới thiệu qua những tính năng cơ bản nhất của package net/http. Ở bài viết sau ta sẽ cùng tìm hiểu các tính năng nâng cao hơn làm nên sức mạnh của net/http như middleware, context,..

Tham khảo