您好,登錄后才能下訂單哦!
使用 Go 的庫非常容易實現一個 Web 服務器。
這是一個迷你服務器,返回訪問服務器的 URL 的路徑部分。例如,如果請求的 URL 是 http://localhost:8000/hello
,響應將是 URL.Path= "/hello"
。
下面是完整程序的程序:
// 迷你回聲服務器
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
fmt.Println("http://localhost:8000/hello")
http.HandleFunc("/", handler) // 回聲請求調用處理程序
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// 處理非持續回顯請求 URL r 的路徑部分
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
}
請求的 URL 的路徑就是 r.URL.Path
。
為服務器添加功能很容易。一個有用的擴展是一個特定的 URL,下面的版本對 /count 請求會有特殊的響應:
// 迷你回聲和計數器服務器
package main
import (
"fmt"
"log"
"net/http"
"sync"
)
var mu sync.Mutex
var count int
func main() {
fmt.Println("http://localhost:8000/hello")
http.HandleFunc("/", handler)
fmt.Println("http://localhost:8000/count")
http.HandleFunc("/count", counter)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// 處理程序回顯請求的 URL 的路徑部分
func handler(w http.ResponseWriter, r *http.Request) {
mu.Lock()
count++
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path)
mu.Unlock()
}
// 回顯目前為止調用的次數
func counter(w http.ResponseWriter, r *http.Request) {
mu.Lock()
fmt.Fprintf(w, "Count %d\n", count)
mu.Unlock()
}
這個服務器有兩個處理函數,通過請求的 URL 來決定哪一個被調用。
下面這個示例中的處理函數,報告它接收到的請求頭和表單數據,這樣還方便服務器審查和調試請求:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
fmt.Println("http://localhost:8000/?k1=v1&k2=v2&k3=1&k3=2&k3=3")
http.HandleFunc("/", handler)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
// 處理程序回顯 HTTP 請求
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s %s %s\n", r.Method, r.URL, r.Proto)
for k, v := range r.Header {
fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
}
fmt.Fprintf(w, "Host = %q\n", r.Host)
fmt.Fprintf(w, "RemoteAddr = %q\n", r.RemoteAddr)
if err := r.ParseForm(); err != nil {
log.Print(err)
}
for k, v := range r.Form {
fmt.Fprintf(w, "Form[%q] = %q\n", k, v)
}
}
這里匯報了很多的內容:
進一步了解基于 http.Handler 接口的服務器API。
下面是源碼中接口的定義:
package http
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
ListenAndServe 函數,這里關注接口,只看函數的簽名,忽略函數體的內容。函數的第二個參數接收一個 Handler 接口的實例(用來接受所有的請求)。這個函數會一直執行,直到服務出錯時返回一個非空的錯誤值。
下面的程序展示一個簡單的例子。使用map類型的database變量記錄商品和價格的映射。再加上一個 ServeHTTP 方法來滿足 http.Handler 接口。這個函數遍歷整個 map 并且輸出其中的元素:
package main
import (
"fmt"
"log"
"net/http"
)
type dollars float32
func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func main() {
db := database{"shoes": 50, "socks": 5}
fmt.Println("http://localhost:8000")
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
上面的示例中,服務器只能列出所有的商品,并且完全不管 URL,對每個請求都是同樣的功能。一般的 Web 服務會定義過個不同的 URL,每個觸發不同的行為。把現有的功能的 URL 設置為 /list,再加上另一個 /price 用來顯示單個商品的價格,商品可以在請求參數中指定,比如:/price?item=socks
:
package main
import (
"fmt"
"log"
"net/http"
)
type dollars float32
func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
type database map[string]dollars
func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
case "/price":
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such item: %q\n", item)
// 也可以用 http.Error 實現上面2行的效果
// http.Error(w, fmt.Sprintf("no such item: %q\n", item), http.StatusNotFound)
return
}
fmt.Fprintf(w, "%s\n", price)
default:
w.WriteHeader(http.StatusNotFound) // 404
fmt.Fprintf(w, "no such page: %s\n", req.URL)
// http.Error(w, fmt.Sprintf("no such page: %s\n", req.URL), http.StatusNotFound)
}
}
func main() {
db := database{"shoes": 50, "socks": 5}
fmt.Println("http://localhost:8000/list")
fmt.Println("http://localhost:8000/price?item=shoes")
log.Fatal(http.ListenAndServe("localhost:8000", db))
}
現在,處理函數基于 URL 的路徑部分(req.URL.Path)來決定執行哪部分邏輯。
返回錯誤頁面 404
如果處理函數不能識別這個路徑,那么它通過調用w.WriteHeader(http.StatusNotFound)
來返回一個 HTTP 錯誤。這個調用必須在網 w 中寫入內容之前執行。這里還可以使用 http.Error 這個工具函數了達到同樣的目的:
msg := fmt.Sprintf("no such item: %q\n", item)
http.Error(w, msg, http.StatusNotFound) // 404
Get請求參數
對應 /price 的場景,它調用了 URL 的 Query 方法,把 HTTP 的請求參數解析為一個map,或者更精確來講,解析為一個 multimap,由 net/url 包的 url.Values 類型實現。這里的 url.Values 是一個 map 映射:
type Values map[string][]string
它的 value 是一個 字符串切片,這里用了 Get 方法,只會提取切片的第一個值。如果是要提取某個 key 所有的值,簡單的通過 map 的 key 提取 value 應該就好了。
如果要繼續給 ServeHTTP 方法添加功能,應當把每部分邏輯分到獨立的函數或方法。net/http 包提供了一個請求多工轉發器 ServeMux,用來簡化 URL 和處理程序之間的關聯。一個 ServeMux 把多個 http.Handler 組合成單個 http.Handler。在這里,可以看到滿足同一個接口的多個類型是可以互相替代的,Web 服務器可以把請求分發到任意一個 http.Handlr,而不用管后面具體的類型。
對于更加復雜的應用,多個 ServeMux 會組合起來,用來處理更復雜的分發需求。Go 語言并不需要一個類似于 Python 的 Django 那樣的權威 Web 框架。因為 Go 語言的標準庫提供的基礎單元足夠靈活,以至于那樣的框架通常不是必須的。進一步來了講,盡管框架在項目初期帶來很多便利,但框架帶來了額外復雜性,增加長時間維護的難度。不過這樣的Web框架也是有的,比如:beego。
將程序修改為使用 ServeMux,用于將 /list、/prics 這樣的 URL 和對應的處理程序關聯起來,這些處理程序也已經拆分到不同的方法中。最后作為主處理程序在 ListenAndServe 調用中使用這個 ServeMux:
package main
import (
"fmt"
"log"
"net/http"
)
type dollars float32
func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
type database map[string]dollars
func (db database) list(w http.ResponseWriter, req *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func (db database) price(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
http.Error(w, fmt.Sprintf("no such item: %q\n", item), http.StatusNotFound)
return
}
fmt.Fprintf(w, "%s\n", price)
}
func main() {
db := database{"shoes": 50, "socks": 5}
fmt.Println("http://localhost:8000/list")
fmt.Println("http://localhost:8000/price?item=shoes")
mux := http.NewServeMux()
mux.Handle("/list", http.HandlerFunc(db.list))
mux.Handle("/price", http.HandlerFunc(db.price))
log.Fatal(http.ListenAndServe("localhost:8000", mux))
}
注冊處理程序
先關注一下用于注冊程序的兩次 mux.Handle 調用。在第一個調用中,db.list是一個方法值,即如下類型的一個值:
func(w http.ResponseWriter, req *http.Request)
當調用 db.list 時,等價于以 db 為接收者調用 database.list 方法。所以 db.list 是一個實現了處理功能的函數。然而他沒有接口所需的方法,所以它不滿足 http.Handler 接口,也不能直接傳給 mux.Handle。
表達式http.HandlerFunc(db.list)
其實是一個類型轉換,而不是函數調用。注意,http.HandlerFunc 是一個類型,它有如下定義:
package http
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
http.HandlerFunc 這個函數類型它有自己的 ServeHTTP 方法,因此它滿足接口。而 http.HandlerFunc 的函數簽名和 db.list 這個方法值的函數簽名是一樣的,因此也能夠進行類型轉換。
這個是 Go 語言接口機制的一個不常見的特性。它不僅是一個函數類型,還可以擁有自己的方法,它的 ServeHTTP 方法就是調用函數本身,所以 HandlerFunc 是一個讓函數值滿足接口的一個適配器(關于適配器,我另外單獨寫了一篇:https://blog.51cto.com/steed/2392540 )。在這個例子里,函數和接口的唯一方法擁有同樣的簽名。這個小技巧讓 database 類型可以用不同的方式來滿足 http.Handler 接口,一次通過 list 方法,一次通過 price 方法。
因為這種注冊處理程序的方法太常見了,所以 ServeMux 引入了一個 HandleFunc 便捷方法來簡化調用,處理程序注冊部分的代碼可以簡化為如下的形式:
// mux.Handle("/list", http.HandlerFunc(db.list))
mux.HandleFunc("/list", db.list)
// mux.Handle("/prics", http.HandlerFunc(db.price))
mux.HandleFunc("/price", db.price)
全局 ServeMux 實例
通過 ServeMux,如果需要有兩個不同的 Web 服務,在不同的端口監聽。那么就定義不同的 URL,分發到不同的處理程序。只須簡單地構造兩個 ServeMux,再調用一次 ListenAndServe 即可(建議并發調用)。不過很多時候一個 Web 服務足夠了,另外也不需要多個 ServeMux 實例。對于這種簡單的應用場景,建議用下面的簡化的調用方法。
net/http 包還提供了一個全局的 ServeMux 實例 DefaultServeMux,以及包級別的注冊函數 http.Handle 和 http.HandleFunc。要讓 DefaultServeMux 作為服務器的主處理程序,無須把它傳給 ListenAndServe,直接傳nil即可。文章開頭的例子里就是這么用的。
服務器的主函數可以進一步簡化:
func main() {
db := database{"shoes": 50, "socks": 5}
http.HandleFunc("/list", db.list)
http.HandleFunc("/price", db.price)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
Web 服務器每次都用一個新的 goroutine 來調用處理程序,所以處理程序必須要注意并發問題。比如在訪問變量時的鎖問題,這個變量可能會被其他 goroutine 訪問,包括由同一個處理程序出廠的其他請求。文章開頭的第二個例子就要類似的處理。
并發安全是另外一塊內容,需要單獨研究和解決,這里去簡單提一下。如果要添加創建、更新商品的功能,就需要注意并發安全。
功能需求
增加額外的處理程序,來支持創建、讀取、更新和刪除數據庫條目。比如,/update?item=socke&price=6
這樣的請求將更新倉庫中物品的價格,如果商品不存在或者價格無效就返回錯誤。(注意:這次修改會引入并發變量修改。)
Go 語言有兩種實現并發安全的方式,這里通過加鎖來保證并發安全:
package main
import (
"errors"
"fmt"
"log"
"net/http"
"strconv"
"sync"
)
type dollars float32
func (d dollars) String() string { return fmt.Sprintf("$%.2f", d) }
type database struct {
items map[string]dollars
sync.RWMutex
}
func (db *database) list(w http.ResponseWriter, req *http.Request) {
db.RLock()
defer db.RUnlock()
for item, price := range db.items {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
}
func (db *database) price(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get("item")
db.RLock()
defer db.RUnlock()
price, ok := db.items[item]
if !ok {
http.Error(w, fmt.Sprintf("no such item: %q\n", item), http.StatusNotFound)
return
}
fmt.Fprintf(w, "%s\n", price)
}
// 從 URL 解析獲取item和price
func getItemPrice(req *http.Request) (string, dollars, error) {
item := req.URL.Query().Get("item")
if item == "" {
return "", 0, errors.New("item not get")
}
priceStr := req.URL.Query().Get("price")
if priceStr == "" {
return item, 0, errors.New("price not get")
}
price64, err := strconv.ParseFloat(priceStr, 32)
price := dollars(price64)
if err != nil {
return item, price, fmt.Errorf("Parse Price: %v\n", err)
}
return item, price, err
}
func (db *database) add(w http.ResponseWriter, req *http.Request) {
item, price, err := getItemPrice(req)
if err != nil {
http.Error(w, fmt.Sprintln(err), http.StatusNotFound)
return
}
db.Lock()
defer db.Unlock()
if _, ok := db.items[item]; ok {
http.Error(w, fmt.Sprintf("%s is already exist.\n", item), http.StatusNotFound)
return
}
db.items[item] = dollars(price)
fmt.Fprintf(w, "success add %s: %s\n", item, dollars(price))
}
func (db *database) update(w http.ResponseWriter, req *http.Request) {
item, price, err := getItemPrice(req)
if err != nil {
http.Error(w, fmt.Sprintln(err), http.StatusNotFound)
return
}
db.Lock()
defer db.Unlock()
if _, ok := db.items[item]; !ok {
http.Error(w, fmt.Sprintf("%s is not exist.\n", item), http.StatusNotFound)
return
}
db.items[item] = dollars(price)
fmt.Fprintf(w, "success udate %s: %s\n", item, dollars(price))
}
func (db *database) delete(w http.ResponseWriter, req *http.Request) {
item := req.URL.Query().Get("item")
func () {
db.Lock()
defer db.Unlock()
delete(db.items, item)
}()
db.list(w, req)
}
func main() {
db := database{
items: map[string]dollars{"shoes": 50, "socks": 5},
}
fmt.Println("http://localhost:8000/list")
fmt.Println("http://localhost:8000/price?item=shoes")
fmt.Println("http://localhost:8000/add?item=football&price=11")
fmt.Println("http://localhost:8000/update?item=football&price=12.35")
fmt.Println("http://localhost:8000/delete?item=shoes")
http.HandleFunc("/list", db.list)
http.HandleFunc("/price", db.price)
http.HandleFunc("/add", db.add)
http.HandleFunc("/update", db.update)
http.HandleFunc("/delete", db.delete)
log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
不但新增的創建、更新和刪除的方法要加鎖,因為現在有了并發安全問題,原本的讀取方法也需要加鎖,才能保證讀取到的數據是當前最新的。
這部分內容是從別處收集來了。
Go 語言原生支持 http,所有 Go 的http服務性能和nginx比較接近。如果用 Go 寫的 Web 程序上線,程序前面不需要再部署nginx的Web服務器,這樣就省掉的是Web服務器。這是單應用的部署。
對于多應用部署,服務器需要部署多個Web應用,這時就需要反向代理了,一般這也是nginx或apache。
反向代理,有個很棒的說法是流量轉發。我獲取到客戶端來的請求,將它發往另一個服務器,從服務器獲取到響應再回給原先的客戶端。反向的意義簡單來說在于這個代理自身決定了何時將流量發往何處。
Go 的反向代理,可以參考下這篇。1 行 Go 代碼實現反向代理:
https://studygolang.com/articles/14246
下面是我之前寫的另一篇有個 HTTP 服務端內容的,主要是這篇里的Panic 處理這個小章節,讓程序可以在處理函數發生崩潰之后可以通過 revoer 來自動恢復:
https://blog.51cto.com/steed/2321827
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。