Go 語言 Web 開發(fā)簡介

2023-03-22 15:03 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch5-web/ch5-01-introduction.html


5.1 Web 開發(fā)簡介

因為 Go 的 net/http 包提供了基礎的路由函數組合與豐富的功能函數。所以在社區(qū)里流行一種用 Go 編寫 API 不需要框架的觀點,在我們看來,如果你的項目的路由在個位數、URI 固定且不通過 URI 來傳遞參數,那么確實使用官方庫也就足夠。但在復雜場景下,官方的 http 庫還是有些力有不逮。例如下面這樣的路由:

GET   /card/:id
POST  /card/:id
DELTE /card/:id
GET   /card/:id/name
...
GET   /card/:id/relations

可見是否使用框架還是要具體問題具體分析的。

Go 的 Web 框架大致可以分為這么兩類:

  1. Router 框架
  2. MVC 類框架

在框架的選擇上,大多數情況下都是依照個人的喜好和公司的技術棧。例如公司有很多技術人員是 PHP 出身,那么他們一定會非常喜歡像 beego 這樣的框架,但如果公司有很多 C 程序員,那么他們的想法可能是越簡單越好。比如很多大廠的 C 程序員甚至可能都會去用 C 語言去寫很小的 CGI 程序,他們可能本身并沒有什么意愿去學習 MVC 或者更復雜的 Web 框架,他們需要的只是一個非常簡單的路由(甚至連路由都不需要,只需要一個基礎的 HTTP 協(xié)議處理庫來幫他省掉沒什么意思的體力勞動)。

Go 的 net/http 包提供的就是這樣的基礎功能,寫一個簡單的 http echo server 只需要 30s。

//brief_intro/echo.go
package main
import (...)

func echo(wr http.ResponseWriter, r *http.Request) {
    msg, err := ioutil.ReadAll(r.Body)
    if err != nil {
        wr.Write([]byte("echo error"))
        return
    }

    writeLen, err := wr.Write(msg)
    if err != nil || writeLen != len(msg) {
        log.Println(err, "write len:", writeLen)
    }
}

func main() {
    http.HandleFunc("/", echo)
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal(err)
    }
}

如果你過了 30s 還沒有完成這個程序,請檢查一下你自己的打字速度是不是慢了(開個玩笑 :D)。這個例子是為了說明在 Go 中寫一個 HTTP 協(xié)議的小程序有多么簡單。如果你面臨的情況比較復雜,例如幾十個接口的企業(yè)級應用,直接用 net/http 庫就顯得不太合適了。

我們來看看開源社區(qū)中一個 Kafka 監(jiān)控項目中的做法:

//Burrow: http_server.go
func NewHttpServer(app *ApplicationContext) (*HttpServer, error) {
    ...
    server.mux.HandleFunc("/", handleDefault)

    server.mux.HandleFunc("/burrow/admin", handleAdmin)

    server.mux.Handle("/v2/kafka", appHandler{server.app, handleClusterList})
    server.mux.Handle("/v2/kafka/", appHandler{server.app, handleKafka})
    server.mux.Handle("/v2/zookeeper", appHandler{server.app, handleClusterList})
    ...
}

上面這段代碼來自大名鼎鼎的 linkedin 公司的 Kafka 監(jiān)控項目 Burrow,沒有使用任何 router 框架,只使用了 net/http。只看上面這段代碼似乎非常優(yōu)雅,我們的項目里大概只有這五個簡單的 URI,所以我們提供的服務就是下面這個樣子:

/
/burrow/admin
/v2/kafka
/v2/kafka/
/v2/zookeeper

如果你確實這么想的話就被騙了。我們再進 handleKafka() 這個函數一探究竟:

func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request) (int, string) {
    pathParts := strings.Split(r.URL.Path[1:], "/")
    if _, ok := app.Config.Kafka[pathParts[2]]; !ok {
        return makeErrorResponse(http.StatusNotFound, "cluster not found", w, r)
    }
    if pathParts[2] == "" {
        // Allow a trailing / on requests
        return handleClusterList(app, w, r)
    }
    if (len(pathParts) == 3) || (pathParts[3] == "") {
        return handleClusterDetail(app, w, r, pathParts[2])
    }

    switch pathParts[3] {
    case "consumer":
        switch {
        case r.Method == "DELETE":
            switch {
            case (len(pathParts) == 5) || (pathParts[5] == ""):
                return handleConsumerDrop(app, w, r, pathParts[2], pathParts[4])
            default:
                return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
            }
        case r.Method == "GET":
            switch {
            case (len(pathParts) == 4) || (pathParts[4] == ""):
                return handleConsumerList(app, w, r, pathParts[2])
            case (len(pathParts) == 5) || (pathParts[5] == ""):
                // Consumer detail - list of consumer streams/hosts? Can be config info later
                return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
            case pathParts[5] == "topic":
                switch {
                case (len(pathParts) == 6) || (pathParts[6] == ""):
                    return handleConsumerTopicList(app, w, r, pathParts[2], pathParts[4])
                case (len(pathParts) == 7) || (pathParts[7] == ""):
                    return handleConsumerTopicDetail(app, w, r, pathParts[2], pathParts[4], pathParts[6])
                }
            case pathParts[5] == "status":
                return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], false)
            case pathParts[5] == "lag":
                return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], true)
            }
        default:
            return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
        }
    case "topic":
        switch {
        case r.Method != "GET":
            return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
        case (len(pathParts) == 4) || (pathParts[4] == ""):
            return handleBrokerTopicList(app, w, r, pathParts[2])
        case (len(pathParts) == 5) || (pathParts[5] == ""):
            return handleBrokerTopicDetail(app, w, r, pathParts[2], pathParts[4])
        }
    case "offsets":
        // Reserving this endpoint to implement later
        return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
    }

    // If we fell through, return a 404
    return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
}

因為默認的 net/http 包中的 mux 不支持帶參數的路由,所以 Burrow 這個項目使用了非常蹩腳的字符串 Split 和亂七八糟的 switch case 來達到自己的目的,但卻讓本來應該很集中的路由管理邏輯變得復雜,散落在系統(tǒng)的各處,難以維護和管理。如果讀者細心地看過這些代碼之后,可能會發(fā)現其它的幾個 handler 函數邏輯上較簡單,最復雜的也就是這個 handleKafka()。而我們的系統(tǒng)總是從這樣微不足道的混亂開始積少成多,最終變得難以收拾。

根據我們的經驗,簡單地來說,只要你的路由帶有參數,并且這個項目的 API 數目超過了 10,就盡量不要使用 net/http 中默認的路由。在 Go 開源界應用最廣泛的 router 是 httpRouter,很多開源的 router 框架都是基于 httpRouter 進行一定程度的改造的成果。關于 httpRouter 路由的原理,會在本章節(jié)的 router 一節(jié)中進行詳細的闡釋。

再來回顧一下文章開頭說的,開源界有這么幾種框架,第一種是對 httpRouter 進行簡單的封裝,然后提供定制的中間件和一些簡單的小工具集成比如 gin,主打輕量,易學,高性能。第二種是借鑒其它語言的編程風格的一些 MVC 類框架,例如 beego,方便從其它語言遷移過來的程序員快速上手,快速開發(fā)。還有一些框架功能更為強大,除了數據庫 schema 設計,大部分代碼直接生成,例如 goa。不管哪種框架,適合開發(fā)者背景的就是最好的。

本章的內容除了會展開講解 router 和中間件的原理外,還會以現在工程界面臨的問題結合 Go 來進行一些實踐性的說明。希望能夠對沒有接觸過相關內容的讀者有所幫助。



以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號