Go Web 服务

骑士救兵 · · 1189 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

一个 Web 服务器

使用 Go 的库非常容易实现一个 Web 服务器。

请求的 URL 路径

这是一个迷你服务器,返回访问服务器的 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)
    }
}

这里汇报了很多的内容:

  • 请求方法 : r.Method
  • 请求路径 : r.URL,这里就是 r.URL.Path。r.URL是个结构体,这里应该只有 Path 字段有内容。然后 %s 是调用它的 String 方法输出
  • 请求协议 : r.Proto
  • 请求头 : r.Header,这是个 map,这里一项一项输出了
  • 服务端地址 : r.Host,包括主机名和端口号
  • 客户端地址 : r.RemoteAddr,包括主机名和端口号
  • 表单信息 : r.Form,这个先要用 r.ParseForm() 进行解析后才会有内容。包括 Get 请求和 Post 请求的信息都会在 r.Form 这个 map 里。

http.Handler 接口

进一步了解基于 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 是一个让函数值满足接口的一个适配器(关于适配器,我会在下一遍单独讲)。在这个例子里,函数和接口的唯一方法拥有同样的签名。这个小技巧让 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

Panic 处理

下面是我之前写的另一篇有个 HTTP 服务端内容的,主要是这篇里的Panic 处理这个小章节,让程序可以在处理函数发生崩溃之后可以通过 revoer 来自动恢复:
https://blog.51cto.com/steed/2321827


有疑问加站长微信联系(非本文作者)

本文来自:51CTO博客

感谢作者:骑士救兵

查看原文:Go Web 服务

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1189 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传