使用 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进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。