运维八一 运维八一
首页
运维杂记
编程浅尝
周积跬步
专栏
生活
关于
收藏
  • 分类
  • 标签
  • 归档
Source (opens new window)

运维八一

运维,运维!
首页
运维杂记
编程浅尝
周积跬步
专栏
生活
关于
收藏
  • 分类
  • 标签
  • 归档
Source (opens new window)
  • Go

    • 前言

    • Go基础知识

    • Go基本语法

    • 实战项目:简单web服务

      • 最简单的HTTP服务
      • 图书管理 API 服务
        • 2. 图书管理 API 服务
          • 2.1 项目建立与布局设计
          • 2.2 项目 main 包
          • 2.3 图书数据存储模块(store)
          • 2.4 HTTP 服务模块(server)
          • 2.5 编译、运行与验证
    • 基本数据类型

    • 内置运算符

    • 分支和循环

    • 函数 function

    • 结构体 struct

    • 方法 method

    • 实战项目:跟踪函数调用链

    • 接口 interface

    • 并发 concurrency

    • 指针

    • 实战项目:实现轻量级线程池

    • 实战项目:实现TCP服务器

    • go常用包

    • Gin框架

    • go随记

  • Python

  • Shell

  • Java

  • Vue

  • 前端

  • 编程浅尝
  • Go
  • 实战项目:简单web服务
lyndon
2022-06-07
目录

图书管理 API 服务

# 2. 图书管理 API 服务

模拟真实世界的一个书店的图书管理后端服务。这个服务为平台前端以及其他客户端,提供针对图书的 CRUD(创建、检索、更新与删除)的基于 HTTP 协议的 API。

API 采用典型的 RESTful 风格设计,这个服务提供的 API 集合如下:

img

# 2.1 项目建立与布局设计

创建一个名为 bookstore 的 Go 项目并创建对应的 Go Module:

$mkdir bookstore
$cd bookstore
$go mod init bookstore
go: creating new go.mod: module bookstore
1
2
3
4

服务大体拆分为两大部分:

  • 一部分是 HTTP 服务器,用来对外提供 API 服务;
  • 一部分是图书数据的存储模块,所有的图书数据均存储在这里。

项目的结构布局:

├── cmd/
│   └── bookstore/         // 放置bookstore main包源码
│       └── main.go
├── go.mod                 // module bookstore的go.mod
├── go.sum
├── internal/              // 存放项目内部包的目录
│   └── store/
│       └── memstore.go     
├── server/                // HTTP服务器模块
│   ├── middleware/
│   │   └── middleware.go
│   └── server.go          
└── store/                 // 图书数据存储模块
    ├── factory/
    │   └── factory.go
    └── store.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2.2 项目 main 包

main 包是主要包,main 包的实现逻辑图:

img

main.go:

  package main
 
  import (
      _ "bookstore/internal/store"
      "bookstore/server"
      "bookstore/store/factory"
      "context"
      "log"
      "os"
      "os/signal"
      "syscall"
      "time"
 )
 
 func main() {
     s, err := factory.New("mem") // 创建图书数据存储模块实例
     if err != nil {
         panic(err)
     }
 
     srv := server.NewBookStoreServer(":8080", s) // 创建http服务实例
 
     errChan, err := srv.ListenAndServe() // 运行http服务
     if err != nil {
         log.Println("web server start failed:", err)
         return
     }
     log.Println("web server start ok")
 
     c := make(chan os.Signal, 1)
     signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
 
     select { // 监视来自errChan以及c的事件
     case err = <-errChan:
         log.Println("web server run failed:", err)
         return
     case <-c:
         log.Println("bookstore program is exiting...")
         ctx, cf := context.WithTimeout(context.Background(), time.Second)
         defer cf()
         err = srv.Shutdown(ctx) // 优雅关闭http服务实例
     }
 
     if err != nil {
         log.Println("bookstore program exit error:", err)
         return
     }
     log.Println("bookstore program exit ok")
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

在 Go 中,main 包不仅包含了整个程序的入口,它还是整个程序中主要模块初始化与组装的场所。

对应在这个程序中,主要模块就是第 16 行的创建图书存储模块实例,以及第 21 行创建 HTTP 服务模块实例。而且,还要注意的是,第 21 行创建 HTTP 服务模块实例的时候,把图书数据存储实例 s 作为参数,传递给了 NewBookStoreServer 函数。

main 函数的后半部分(第 30 行~ 第 42 行),通过监视系统信号实现了 http 服务实例的优雅退出。

所谓优雅退出,指的就是程序有机会等待其他的事情处理完再退出。比如尚未完成的事务处理、清理资源(比如关闭文件描述符、关闭 socket)、保存必要中间状态、内存数据持久化落盘,等等。如果你经常用 Go 来编写 http 服务,那么 http 服务如何优雅退出,就是你经常要考虑的问题。

在这个问题的具体实现上,通过 signal 包的 Notify 捕获了 SIGINT、SIGTERM 这两个系统信号。这样,当这两个信号中的任何一个触发时,http 服务实例都有机会在退出前做一些清理工作。然后,再使用 http 服务实例(srv)自身提供的 Shutdown 方法,来实现 http 服务实例内部的退出清理工作,包括:立即关闭所有 listener、关闭所有空闲的连接、等待处于活动状态的连接处理完毕,等等。当 http 服务实例的清理工作完成后,整个程序就可以正常退出了。

# 2.3 图书数据存储模块(store)

图书数据存储模块的职责很清晰,就是用来存储整个 bookstore 的图书数据的。图书数据存储有很多种实现方式,最简单的方式莫过于在内存中创建一个 map,以图书 id 作为 key,来保存图书信息。

如果考虑上生产环境,数据要进行持久化,那么最实际的方式就是通过 Nosql 数据库甚至是关系型数据库,实现对图书数据的存储与管理。

针对图书的有限种存储操作,放置在一个接口类型 Store 中:

// store/store.go

 type Book struct {
     Id      string   `json:"id"`      // 图书ISBN ID
     Name    string   `json:"name"`    // 图书名称
     Authors []string `json:"authors"` // 图书作者
     Press   string   `json:"press"`   // 出版社
 }
 
 type Store interface {
     Create(*Book) error        // 创建一个新图书条目
     Update(*Book) error        // 更新某图书条目
     Get(string) (Book, error)  // 获取某图书信息
     GetAll() ([]Book, error)   // 获取所有图书信息
     Delete(string) error       // 删除某图书条目
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

建立了一个对应图书条目的抽象数据类型 Book,以及针对 Book 存取的接口类型 Store。这样,对于想要进行图书数据操作的一方来说,他只需要得到一个满足 Store 接口的实例,就可以实现对图书数据的存储操作了,不用再关心图书数据究竟采用了何种存储方式。这就实现了图书存储操作与底层图书数据存储方式的解耦。而且,这种面向接口编程也是 Go 组合设计哲学的一个重要体现。

创建一个满足 Store 接口的实例,store/factory 包:

// store/factory/factory.go

 var (
     providersMu sync.RWMutex
     providers   = make(map[string]store.Store)
 )
 
 func Register(name string, p store.Store) {
     providersMu.Lock()
    defer providersMu.Unlock()
     if p == nil {
         panic("store: Register provider is nil")
     }
 
     if _, dup := providers[name]; dup {
         panic("store: Register called twice for provider " + name)
     }
     providers[name] = p
 }
 
 func New(providerName string) (store.Store, error) {
     providersMu.RLock()
     p, ok := providers[providerName]
     providersMu.RUnlock()
     if !ok {
         return nil, fmt.Errorf("store: unknown provider %s", providerName)
     }
 
     return p, nil
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

这段代码实际上是效仿了 Go 标准库的 database/sql 包采用的方式,factory 包采用了一个 map 类型数据,对工厂可以“生产”的、满足 Store 接口的实例类型进行管理。factory 包还提供了 Register 函数,让各个实现 Store 接口的类型可以把自己“注册”到工厂中来。一旦注册成功,factory 包就可以“生产”出这种满足 Store 接口的类型实例。而依赖 Store 接口的使用方,只需要调用 factory 包的 New 函数,再传入期望使用的图书存储实现的名称,就可以得到对应的类型实例了。

在项目的 internal/store 目录下,还提供了一个基于内存 map 的 Store 接口的实现,它能够自注册到 factory 包中的:

// internal/store/memstore.go

 package store
  
 import (
     mystore "bookstore/store"
     factory "bookstore/store/factory"
     "sync"
 )
  
 func init() {
     factory.Register("mem", &MemStore{
         books: make(map[string]*mystore.Book),
     })
 }
 
 type MemStore struct {
     sync.RWMutex
     books map[string]*mystore.Book
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

从 memstore 的代码来看,它是在包的 init 函数中调用 factory 包提供的 Register 函数,把自己的实例以“mem”的名称注册到 factory 中的。这样做有一个好处,依赖 Store 接口进行图书数据管理的一方,只要导入 internal/store 这个包,就可以自动完成注册动作了。

# 2.4 HTTP 服务模块(server)

HTTP 服务模块的职责是对外提供 HTTP API 服务,处理来自客户端的各种请求,并通过 Store 接口实例执行针对图书数据的相关操作。

抽象处理一个 server 包,这个包中定义了一个 BookStoreServer 类型如下:

// server/server.go

 type BookStoreServer struct {
     s   store.Store
     srv *http.Server
 }
1
2
3
4
5
6

这个类型实质上就是一个标准库的 http.Server,并且组合了来自 store.Store 接口的能力。

server 包提供了 NewBookStoreServer 函数,用来创建一个 BookStoreServer 类型实例:

// server/server.go

 func NewBookStoreServer(addr string, s store.Store) *BookStoreServer {
     srv := &BookStoreServer{
         s: s,
         srv: &http.Server{
             Addr: addr,
         },
     }
 
     router := mux.NewRouter()
     router.HandleFunc("/book", srv.createBookHandler).Methods("POST")
     router.HandleFunc("/book/{id}", srv.updateBookHandler).Methods("POST")
     router.HandleFunc("/book/{id}", srv.getBookHandler).Methods("GET")
     router.HandleFunc("/book", srv.getAllBooksHandler).Methods("GET")
     router.HandleFunc("/book/{id}", srv.delBookHandler).Methods("DELETE")
 
     srv.srv.Handler = middleware.Logging(middleware.Validating(router))
     return srv
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

函数 NewBookStoreServer 接受两个参数,一个是 HTTP 服务监听的服务地址,另外一个是实现了 store.Store 接口的类型实例。

这种函数原型的设计是 Go 语言的一种惯用设计方法,也就是接受一个接口类型参数,返回一个具体类型。返回的具体类型组合了传入的接口类型的能力。

借助一个第三方包 github.com/gorilla/mux 来实现设置请求的处理函数,针对不同 URI 路径模式设置了不同的处理函数,以 createBookHandler 和 getBookHandler 为例:

// server/server.go

  func (bs *BookStoreServer) createBookHandler(w http.ResponseWriter, req *http.Request) {
      dec := json.NewDecoder(req.Body)
      var book store.Book
      if err := dec.Decode(&book); err != nil {
          http.Error(w, err.Error(), http.StatusBadRequest)
          return
      }
  
      if err := bs.s.Create(&book); err != nil {
          http.Error(w, err.Error(), http.StatusBadRequest)
          return
      }
  }

  func (bs *BookStoreServer) getBookHandler(w http.ResponseWriter, req *http.Request) {
      id, ok := mux.Vars(req)["id"]
      if !ok {
          http.Error(w, "no id found in request", http.StatusBadRequest)
          return
      }
  
     book, err := bs.s.Get(id)
     if err != nil {
         http.Error(w, err.Error(), http.StatusBadRequest)
         return
     }
 
     response(w, book)
 }

 func response(w http.ResponseWriter, v interface{}) {
     data, err := json.Marshal(v)
     if err != nil {
         http.Error(w, err.Error(), http.StatusInternalServerError)
         return
     }
     w.Header().Set("Content-Type", "application/json")
     w.Write(data)
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

这些处理函数的实现都大同小异,都是先获取 http 请求包体数据,然后通过标准库 json 包将这些数据,解码(decode)为需要的 store.Book 结构体实例,再通过 Store 接口对图书数据进行存储操作。如果是获取图书数据的请求,那么处理函数将通过 response 函数,把取出的图书数据编码到 http 响应的包体中,并返回给客户端。

两个通用的 http 处理函数 middleware,Logging 与 Validating 函数的实现:

// server/middleware/middleware.go

  func Logging(next http.Handler) http.Handler {
     return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
         log.Printf("recv a %s request from %s", req.Method, req.RemoteAddr)
         next.ServeHTTP(w, req)
     })
 }
 
 func Validating(next http.Handler) http.Handler {
     return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
         contentType := req.Header.Get("Content-Type")
         mediatype, _, err := mime.ParseMediaType(contentType)
         if err != nil {
             http.Error(w, err.Error(), http.StatusBadRequest)
             return
         }
         if mediatype != "application/json" {
             http.Error(w, "invalid Content-Type", http.StatusUnsupportedMediaType)
             return
         }
         next.ServeHTTP(w, req)
     })
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

Logging 函数主要用来输出每个到达的 HTTP 请求的一些概要信息,而 Validating 则会对每个 http 请求的头部进行检查,检查 Content-Type 头字段所表示的媒体类型是否为 application/json。这些通用的 middleware 函数,会被串联到每个真正的处理函数之前,避免在每个处理函数中重复实现这些逻辑。

创建完 BookStoreServer 实例后,就可以调用其 ListenAndServe 方法运行这个 http 服务了,显然这个方法的名字是仿效 http.Server 类型的同名方法:

// server/server.go

 func (bs *BookStoreServer) ListenAndServe() (<-chan error, error) {
     var err error
     errChan := make(chan error)
     go func() {
         err = bs.srv.ListenAndServe()
         errChan <- err
     }()
 
     select {
     case err = <-errChan:
         return nil, err
     case <-time.After(time.Second):
         return errChan, nil
     }
 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

这个函数把 BookStoreServer 内部的 http.Server 的运行,放置到一个单独的轻量级线程 Goroutine 中。这是因为,http.Server.ListenAndServe 会阻塞代码的继续运行,如果不把它放在单独的 Goroutine 中,后面的代码将无法得到执行。

为了检测到 http.Server.ListenAndServe 的运行状态,再通过一个 channel(位于第 5 行的 errChan),在新创建的 Goroutine 与主 Goroutine 之间建立通信渠道。通过这个渠道,能及时得到 http server 的运行状态。

# 2.5 编译、运行与验证

因为在程序中引入了一个第三方依赖包,所以在构建项目之前,需要执行下面这个命令,让 Go 命令自动分析依赖项和版本,并更新 go.mod:

$go mod tidy
go: finding module for package github.com/gorilla/mux
go: found github.com/gorilla/mux in github.com/gorilla/mux v1.8.0
1
2
3

构建并执行 bookstore:

$go build bookstore/cmd/bookstore
$./bookstore
2021/10/05 16:08:36 web server start ok
1
2
3

使用 curl 命令行工具,模仿客户端向 bookstore 服务发起请求了,比如创建一个新书条目:

$curl -X POST -H "Content-Type:application/json" -d '{"id": "978-7-111-55842-2", "name": "The Go Programming Language", "authors":["Alan A.A.Donovan", "Brian W. Kergnighan"],"press": "Pearson Education"}' localhost:8080/book
1

此时服务端会输出如下日志,表明 bookstore 服务收到了客户端请求:

2021/10/05 16:09:10 recv a POST request from [::1]:58021
1

再来获取一下这本书的信息:

$curl -X GET -H "Content-Type:application/json" localhost:8080/book/978-7-111-55842-2
{"id":"978-7-111-55842-2","name":"The Go Programming Language","authors":["Alan A.A.Donovan","Brian W. Kergnighan"],"press":"Pearson Education"}
1
2
上次更新: 2022/06/12, 15:48:09
最简单的HTTP服务
内置类型

← 最简单的HTTP服务 内置类型→

最近更新
01
ctr和crictl显示镜像不一致
03-13
02
alpine镜像集成常用数据库客户端
03-13
03
create-cluster
02-26
更多文章>
Theme by Vdoing | Copyright © 2015-2024 op81.com
苏ICP备18041258号-2
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式