您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > Go语言

我是如何在 Go 中构建 Web 服务的

时间:2021-03-11 10:14:17  来源:  作者:

从用了近十年的 C# 转到 Go 是一个有趣的旅程。有时,我陶醉于 Go 的简洁[1];也有些时候,当熟悉的 OOP (面向对象编程)模式[2]无法在 Go 代码中使用的时候会感到沮丧。幸运的是,我已经摸索出了一些写 HTTP 服务的模式,在我的团队中应用地很好。

当在公司项目上工作时,我倾向把可发现性放在最高的优先级上。这些应用会在接下来的 20 年运行在生产环境中,必须有众多的开发人员和网站可靠性工程师(可能是指运维)来进行热补丁,维护和调整工作。因此,我不指望这些模式能适合所有人。

Mat Ryer 的文章[3]是我使用 Go 试验 HTTP 服务的起点之一,也是这篇文章的灵感来源。

代码组成

Broker

一个 Broker 结构是将不同的 service 包绑定到 HTTP 逻辑的胶合结构。没有包作用域结级别的变量被使用。依赖的接口得益于了 Go 的组合[4]的特点被嵌入了进来。

type Broker struct {
    auth.Client             // 从外部仓库导入的身份验证依赖(接口)
    service.Service         // 仓库的业务逻辑包(接口)

    cfg    Config           // 该 API 服务的配置
    router *mux.Router      // 该 API 服务的路由集
}

broker 可以使用阻塞[5]函数 New() 来初始化,该函数校验配置,并且运行所有需要的前置检查。

func New(cfg Config, port int) (*Broker, error) {
    r := &Broker{
        cfg: cfg,
    }

    ...

    r.auth.Client, err = auth.New(cfg.AuthConfig)
    if err != nil {
        return nil, fmt.Errorf("Unable to create new API broker: %w", err)
    }

    ...

    return r, nil
}

初始化后的 Broker 满足了暴露在外的 Server 接口,这些接口定义了所有的,被 route 和 中间件(middleware)使用的功能。service 包接口被嵌入,这些接口与 Broker 上嵌入的接口相匹配。

type Server interface {
    PingDependencies(bool) error
    ValidateJWT(string) error

    service.Service
}

web 服务通过调用 Start() 函数来启动。路由绑定通过一个闭包函数[6]进行绑定,这种方式保证循环依赖不会破坏导入周期规则。

func (bkr *Broker) Start(binder func(s Server, r *mux.Router)) {
    ...

    bkr.router = mux.NewRouter().StrictSlash(true)
    binder(bkr, bkr.router)

    ...

    if err := http.Serve(l, bkr.router); errors.Is(err, http.ErrServerClosed) {
        log.Warn().Err(err).Msg("Web server has shut down")
    } else {
        log.Fatal().Err(err).Msg("Web server has shut down unexpectedly")
    }
}

那些对故障排除(比如,Kubernetes 探针[7])或者灾难恢复方案方面有用的函数,挂在 Broker 上。如果被 routes/middleware 使用的话,这些仅仅被添加到 webserver.Server 接口上。

func (bkr *Broker) SetupDatabase() { ... }
func (bkr *Broker) PingDependencies(failFast bool)) { ... }

启动引导

整个应用的入口是一个 main 包。默认会启动 Web 服务。我们可以通过传入一些命令行参数来调用之前提到的故障排查功能,方便使用传入 New() 函数的,经过验证的配置来测试代理权限以及其他网络问题。我们所要做的只是登入运行着的 pod 然后像使用其他命令行工具一样使用它们。

func main() {
    subCommand := flag.String("start", "", "start the webserver")

    ...

    srv := webserver.New(cfg, 80)

    switch strings.ToLower(subCommand) {
    case "ping":
        srv.PingDependencies(false)
    case "start":
        srv.Start(BindRoutes)
    default:
        fmt.Printf("Unrecognized command %q, exiting.", subCommand)
        os.Exit(1)
    }
}

HTTP 管道设置在 BindRoutes() 函数中完成,该函数通过 ser.Start() 注入到服务(server)中。

func BindRoutes(srv webserver.Server, r *mux.Router) {
    r.Use(middleware.Metrics(), middleware.Authentication(srv))
    r.HandleFunc("/ping", routes.Ping()).Methods(http.MethodGet)

    ...

    r.HandleFunc("/makes/{makeID}/models/{modelID}", model.get(srv)).Methods(http.MethodGet)
}

中间件

中间件(Middleware)返回一个带有 handler 的函数,handler 用来构建需要的 http.HandlerFunc。这使得 webserver.Server 接口被注入,同时所有的安静检查只在启动时执行,而不是在所有路由调用的时候。

func Authentication(srv webserver.Server) func(h http.Handler) http.Handler {
    if srv == nil || !srv.Client.IsValid() {
        log.Fatal().Msg("a nil dependency was passed to authentication middleware")
    }

    // additional setup logic
    ...

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            token := strings.TrimSpace(r.Header.Get("Authorization"))
            if err := srv.ValidateJWT(token); err != nil {
                ...
                w.WriteHeader(401)
                w.Write([]byte("Access Denied"))

                return
            }

            next.ServeHTTP(w, r)
        }
    }
}

路由

路由有着与中间件有着类似的套路——简单的设置,但是有着同样的收益。

func GetLatest(srv webserver.Server) http.HandlerFunc {
    if srv == nil {
        log.Fatal().Msg("a nil dependency was passed to the `/makes/{makeID}/models/{modelID}` route")
    }

    // additional setup logic
    ...

    return func(w http.ResponseWriter, r *http.Request) {
        ...

        makeDTO, err := srv.Get
    }
}

目录结构

代码的目录结构对可发现性进行了高度优化。

├── App/
|   └── service-api/**
├── cmd/
|   └── service-tool-x/
├── internal/
|   └── service/
|       └── mock/
├── pkg/
|   ├── client/
|   └── dtos/
├── (.editorconfig, .gitattributes, .gitignore)
└── go.mod
  • app/ 用于项目应用——这是新来的人了解代码倾向的切入点。dd
    • ./service-api/ 是该仓库的微服务 API;所有的 HTTP 实现细节都在这里。
  • cmd/ 是存放命令行应用的地方。
  • internal/ 是不可以被该仓库以外的项目引入的一个特殊目录[8]
    • ./service/ 是所有领域逻辑(domain logic)所在的地方;可以被 service-api,service-tool-x,以及任何未来直接访问这个目录可以带来收益的应用或者包所引入。
  • pkg/ 用于存放鼓励被仓库以外的项目所引入的包。
    • ./client/ 是用于访问 service-api 的 client 库。其他团队可以使用而不是自己写一个 client,并且我们可以借助我们在 cmd/ 里面的 CI/CD 工具来 “dogfood it[9]” (使用自己产品的意思)。
    • ./dtos/ 是存放项目的数据传输对象,不同包之间共享的数据且以 json 形式在线路上编码或传输的结构体定义。没有从其他仓库包导出的模块化的结构体。/internal/service 负责 这些 DTO (数据传输对象)和自己内部模型的相互映射,避免实现细节的遗漏(如,数据库注释)并且该模型的改变不破坏下游客户端消费这些 DTO。
  • .editorconfig,.gitattributes,.gitignore 因为所有的仓库必须使用 .editorconfig,.gitattributes,.gitignore[10]
  • go.mod 甚至可以在有限制的且官僚的公司环境[11]工作。

最重要的:每个包只负责意见事情,一件事情!

HTTP 服务结构

└── service-api/
    ├── cfg/
    ├── middleware/
    ├── routes/
    |   ├── makes/
    |   |   └── models/**
    |   ├── create.go
    |   ├── create_test.go
    |   ├── get.go
    |   └── get_test.go
    ├── webserver/
    ├── main.go
    └── routebinds.go
  • ./cfg/ 用于存放配置文件,通常是以 JSON 或者 YAML 形式保存的纯文本文件,它们也应该被检入到 Git 里面(除了密码,秘钥等)。
  • ./middleware 用于所有的中间件。
  • ./routes 采用类似应用的类 RESTFul 形式的目录对路由代码进行分组和嵌套。
  • ./webserver 保存所有共享的 HTTP 结构和接口(Broker,配置,Server等等)。
  • main.go 启动应用程序的地方(New(),Start())。
  • routebinds.go BindRoutes() 函数存放的地方。

你觉得呢?

如果你最终采用了这种模式,或者有其他的想法我们可以讨论,我乐意听到这些想法!


via: https://www.dudley.codes/posts/2020.05.19-golang-structure-web-servers/

作者:James Dudley[12]译者:dust347[13]校对:unknwon[14]

本文由 GCTT[15] 原创编译,Go 中文网[16] 荣誉推出

参考资料

[1]

简洁:
https://www.youtube.com/watch?v=rFejpH_tAHM

[2]

模式:
https://en.wikipedia.org/wiki/Software_design_pattern

[3]

Mat Ryer 的文章:
https://pace.dev/blog/2018/05/09/how-I-write-http-services-after-eight-years.html

[4]

Go 的组合:
https://www.ardanlabs.com/blog/2015/09/composition-with-go.html

[5]

阻塞:
https://stackoverflow.com/questions/2407589/what-does-the-term-blocking-mean-in-programming

[6]

闭包函数:
https://gobyexample.com/closures

[7]

Kubernetes 探针:
https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)0

[8]

特殊目录:
https://dave.cheney.net/2019/10/06/use-internal-packages-to-reduce-your-public-api-surface

[9]

dogfood it: https://en.wikipedia.org/wiki/Eating_your_own_dog_food

[10]

所有的仓库必须使用 .editorconfig,.gitattributes,.gitignore:
https://www.dudley.codes/posts/2020.02.16-git-lost-in-translation/

[11]

有限制的且官僚的公司环境:
https://www.dudley.codes/posts/2020.04.02-golang-behind-corporate-firewall/

[12]

James Dudley: https://www.dudley.codes/

[13]

dust347: https://github.com/dust347

[14]

unknwon: https://github.com/unknwon

[15]

GCTT: https://github.com/studygolang/GCTT

[16]

Go 中文网: https://studygolang.com/



Tags:Web 服务   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
从用了近十年的 C# 转到 Go 是一个有趣的旅程。有时,我陶醉于 Go 的简洁[1];也有些时候,当熟悉的 OOP (面向对象编程)模式[2]无法在 Go 代码中使用的时候会感到沮丧。幸运的是,我...【详细内容】
2021-03-11  Tags: Web 服务  点击:(285)  评论:(0)  加入收藏
Caddy 简介Caddy 是一个 Go 编写的 Web 服务器,类似于 Nginx,Caddy 提供了更加强大的功能,随着 v2 版本发布 Caddy 已经可以作为中小型站点 Web 服务器的另一个选择;相较于 Ngin...【详细内容】
2021-03-01  Tags: Web 服务  点击:(168)  评论:(0)  加入收藏
这部分示例将致力于用 Node.js 模拟一个类似于 Apache 的 Web 服务器,处理浏览器端的请求,将相关的页面响应给浏览器。首先,我们要在code目录下执行mkdir 03_webSever命令来创...【详细内容】
2020-08-31  Tags: Web 服务  点击:(63)  评论:(0)  加入收藏
这个 Python 模块可以以多种格式收集网站使用日志并输出良好结构化数据以进行分析。• 来源:linux.cn • 作者:Ben Nuttall • 译者:geekpi •(本文字数:3262,阅...【详细内容】
2020-08-13  Tags: Web 服务  点击:(70)  评论:(0)  加入收藏
▌简易百科推荐
zip 是一种常见的归档格式,本文讲解 Go 如何操作 zip。首先看看 zip 文件是如何工作的。以一个小文件为例:(类 Unix 系统下)$ cat hello.textHello!执行 zip 命令进行归档:$ zip...【详细内容】
2021-12-17  Go语言中文网    Tags:Go语言   点击:(12)  评论:(0)  加入收藏
大家好,我是 polarisxu。前段时间,Russ Cox 明确了泛型相关的事情,原计划在标准库中加入泛型相关的包,改放到 golang.org/x/exp 下。目前,Go 泛型的主要设计者 ianlancetaylor 完...【详细内容】
2021-11-30  Go语言中文网    Tags:slices 包   点击:(24)  评论:(0)  加入收藏
前言最近因为项目需要写了一段时间的 Go ,相对于 Java 来说语法简单同时又有着一些 Python 之类的语法糖,让人大呼”真香“。 但现阶段相对来说还是 Python 写的多一些,偶尔还...【详细内容】
2021-11-25  crossoverJie    Tags:Go   点击:(29)  评论:(0)  加入收藏
go-micro是基于 Go 语言用于开发的微服务的 RPC 框架,主要功能如下:服务发现,负载均衡 ,消息编码,请求/响应,Async Messaging,可插拔接口,最后这个功能牛p安装步骤安装proto...【详细内容】
2021-09-06    石老师小跟班  Tags:go-micro   点击:(196)  评论:(0)  加入收藏
GoLand 2021.2 EAP 5 现已发布。用户可以从工具箱应用程序中获得 EAP 构建,也可以从官方网站手动下载。并且从此 EAP 开始,只有拥有有效的 JetBrains 帐户才能加入该计划。手...【详细内容】
2021-06-29  IT实战联盟  今日头条  Tags:GoLand   点击:(185)  评论:(0)  加入收藏
作者:HDT3213今天给大家带来的开源项目是 Godis:一个用 Go 语言实现的 Redis 服务器。支持: 5 种数据结构(string、list、hash、set、sortedset) 自动过期(TTL) 发布订阅、地理位...【详细内容】
2021-06-18  HelloGitHub  今日头条  Tags:Go   点击:(125)  评论:(0)  加入收藏
统一规范篇合理规划目录本篇主要描述了公司内部同事都必须遵守的一些开发规矩,如统一开发空间,既使用统一的开发工具来保证代码最后的格式的统一,开发中对文件和代码长度的控制...【详细内容】
2021-05-18  1024课堂    Tags:Go语言   点击:(232)  评论:(0)  加入收藏
闭包概述 闭包不是Go语言独有的概念,在很多编程语言中都有闭包 闭包就是解决局部变量不能被外部访问的一种解决方案 是把函数当作返回值的一种应用 代码演示总体思想:在函数...【详细内容】
2021-05-14  HelloGo  今日头条  Tags:Go语言   点击:(223)  评论:(0)  加入收藏
一时想不开,想了解一下Go语言,于是安装了并体验了一下。下载1. 进入golang.google.cn 点击Download Go 2.选择对应的操作系统,点击后开始下载。 安装1. windows下执行傻瓜式安...【详细内容】
2021-05-12  程序员fearlazy  fearlazy  Tags:Go语言   点击:(236)  评论:(0)  加入收藏
1.简介channel是Go语言的一大特性,基于channel有很多值得探讨的问题,如 channel为什么是并发安全的? 同步通道和异步通道有啥区别? 通道为何会阻塞协程? 使用通道导致阻塞的协程...【详细内容】
2021-05-10  程序员麻辣烫  今日头条  Tags:Go通道   点击:(272)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条