一、基于 echo 搭建的简单 http 服务和工程组织结构 - FeifeiyuM/go-microservices-boilerplate GitHub Wiki

基于 echo 搭建的简单 http 服务和工程组织结构

一、http 服务框架选择

echo 基于官网介绍, 其号称是 Golang web 框架界速度最快的框架。但是在实际的工程中, 这点速度上的差异,相比于一次数据库通信,一次第三方通信真的是小巫见大巫,所以笔者认为选择一个 web 框架运行速度不应该成为选型考虑的主要方面。一个框架是否存在致命的缺陷?是否符合业务的需求?是否能够提高开发效率以及是否符合开发者的个人的喜好?反而是比较重要的。
笔者使用过 Golang web 开发框架有大名鼎鼎的 gin;轻量级高性能的路由框架httprouter, gin 的路由模块就是集成了 httprouter ;以及本文将围绕展开的 echo web 框架。
从功能丰富性和生态的成熟度来说 gin 要优于 echo; 从框架复杂程度和学习曲线上来说 httprouter 也是优于 echo。但是,echo 的错误处理方式更符合 Golang 的开发模式,毕竟 Golang 丑陋的错误处理方式,为众多开发者所诟病。

下面 代码1 就是摘抄于 echo 官网的一段代码,在 handler 函数中,我们可以直接 return error 对象,这不正是 Golang 中最常见的错误处理方式,直接向外抛出错误。

package main

import (
    "net/http"
    "github.com/labstack/echo/v4"
)
func main() {
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello, World!")
    })
    e.Logger.Fatal(e.Start(":1323"))
}

代码1

而 gin 就需要开发者在 handler 函数中不能直接抛出错误,需要在函数中处理错误逻辑了,例如:代码2

package main

import "github.com/gin-gonic/gin"
func main() {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(200, gin.H{
            "message": "pong",
        })
    })
    r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

代码2

二、实现一个最小的 http 服务

接下来我们将基于 echo 实现一个最小的 http 服务,将会是一个相对完整的结构,以引出我们接下来要讨论的工程组织结构。

2.1、系统配置

系统配置文件,对应任何一个系统都是不可或缺的,不管是web服务还是客户端应用在启动的一刻或多或少都会读取一些本地配置文件信息。我们通过 代码3 来最简模拟读取配置的方法。

// 定义配置信息对象
type Config struct {
	HttpAddr string // http 端口
}
func InitConfig() *Config {
    // 读取配置文件,并初始化配置文件对象
	config := &Config{
		HttpAddr: "0.0.0.0:8511",
	}
	return config
}

代码3

2.2、handler 函数和路由的实现

路由和其对应的处理函数,是实现一个 http 接口的最基本的构成,该层就是我们熟悉的 MVC 分层结构中对应的 C(Controller) 层,详见 代码4

// 定义一个 handler 对象
type handler struct {}
// 初始化 handler 
func NewHandler() *handler {
	return &handler{}
}
// 实现一个 say hello 方法
func (h *handler) sayHello(c echo.Context) error {
    // 逻辑处理
	name := c.QueryParam("name")
	msg := fmt.Sprintf("Hello %s", name)
	return c.JSON(http.StatusOK, map[string]interface{}{"msg": msg})
}
// 路由配置,将uri 和 handler 方法对应起来
func (h *handler) Router(r *echo.Echo) {
	r.GET("/say-hello", h.sayHello)
}

代码4

2.3、服务的聚合封装

服务的聚合和封装主要是将服务组成的各个部分聚合封装在一起,主要目的是将服务的初始化,启动关闭,以及工程基础相关的功能封装在一起,详见 代码5

// 定义服务封装对象
type server struct {
	echo *echo.Echo  
	cfg *Config
}
// 获取服务封装对象
func NewServer(cfg *Config) *server {
	return &server{
		cfg: cfg,
	}
}
// 初始化 echo 应用
func (s *server) InitEcho() {
	e := echo.New()
	s.echo = e
	// 实例化 handler 层
	handler := NewHandler()
    // 初始化路由
	handler.Router(e)
	
}
// 启动服务
func (s *server) Run() {
	// 初始化 http echo 服务
	s.InitEcho()
	// 启动服务
	go func() 
		err := s.echo.Start(s.cfg.HttpAddr)
		if err != nil {
			panic(err)
		}
	}()
}
// 系统推出时执行 gracefully exit
func (s *server) Close() {
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()
	if s.echo != nil {
		_ = s.echo.Shutdown(ctx)
	}
}

代码5

2.4、服务启动

启动服务是指按顺序执行服务启动的相关流程,从使整个服务开始运行,详见代码6

func StartServer() {
	// 初始配置文件
	cfg := InitConfig()
	// 启动服务
	srv := NewServer(cfg)
	srv.Run()
	// 开启系统信号接收通道
    // 服务进将会结束系统消息,执行程序保持或退出的指令
	c := make(chan os.Signal, 1)
	signal.Notify(c, syscall.SIGHUP, syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT)
	s := <- c
	switch s {
	case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
		srv.Close()
	case syscall.SIGHUP:
	default:
	}
}
// 入口函数
func main() {
	StartServer()
}

代码6

三、工程框架的组织

本文的 Golang 工程组织结构主要遵循 Standard Go Project Layout 的设计, 详细设计逻辑可以参考文档中说明。

工程结构图如图1

project
   |
   ├──cmd/ # 服务启动命令
   ├──docs/ # 文档目录
   ├──internal/  # 私有目录, 服务相关的业务,配置逻辑都集中于该目录下
   |     ├──conf/
   |     |    └──conf.go  # 处理配置相关逻辑
   |     ├──model/ # 模型层
   |     ├──object/ # 模型聚合层
   |     ├──dao/  # 数据持久层
   |     ├──service/ # 业务逻辑层
   |     ├──proto/ # grpc proto 文件定义层
   |     ├──interface/ # 接口层,或者是 controller 层
   |     |      ├──rest/ rest 请求 handler 层
   |     |      ├──rpc/  rpc 请求 handler 层
   |     |      └──mq/  mq 请求 handler 层
   |     └──server.go  # 服务的聚合和封装
   ├──pkg/  # 公共代码库
   ├──main.go  # 程序入口
   ├──config.toml # 工程配置文件
   ├──Makefile  # 工程执行指令
   ├──go.mod  # go mod 文件
   ├──README.md 
   └──.gitignore

图1

1、cmd 目录:用户放置工程的启动命令,在上一节中的 “4、服务启动”相关代码就放置在目录下。比如启动的参数,应用的监控配置等都在该层实现。

2、docs 目录:主要用于放置工程各种文档,比如 数据库 schema, 接口文档等。

3、internal 目录:在 internal 目录下说明这部分代码是工程的私有程序,不应该被外部代码导入的,我们将工程业务逻辑代码都置于该层之下。
服务层的代码,是基于经典的 M(model) V(view) C(controller) 结构,并吸收一部分 DDD (Domain Driver Design) 思想扩展开来的。

  • conf 目录:conf 即配置层,主要用于实现配置文件定义,加载相关的逻辑, 上一节 “2.1、系统配置” 相关逻辑代码就置于该目录下。
  • model 目录:model 即模型层,用于放置存储层的数据对象定义,对一类比较基本稳定的对象定义,比如:数据库中的账号表 account 对应于 model 中 Account 对象。
  • object 目录:object 聚合层,由于 model 层的对象是相对较为稳定,不可能随着业务随意改变的,很多场景需要将多个 model 聚合成为一个对象,或者是一些中间的过度对象。因此引入 object 层来满足这些场景,也可以将 object 层是满足业务层多变的特点所引入。
  • dao 目录: dao 层比较传统,主要用于持久层的操作,比如数据库的增,删,改,查。缓存的写入和读取,消息的发送等。
  • service 目录: service 即业务逻辑层,业务逻辑都在改层实现。
  • interface 目录:interface 即接口层,或者是控制(controller),或者 handler 层,是为了满足 http, rpc, mq 服务间差异而设立的层,用于接收请求,处理参数,鉴权,将格式化的参数送入 service 层处理业务逻辑, 并将处理结果以其各自定义返回。
  • proto 目录: 用于存放 rpc 文件定义,及相关生成代码,这个后面章节将会用到。
  • server.go 文件:主要实现 各个层之间的初始化,聚合等,在上一节中的 “3、服务的聚合封装” 的代码就位于文件中。

4、pkg 目录: 跟 standard Go Project Layout 中定义的一样,主要用于存放一些公共的库,用于服务层调用。
5、main.go 文件: 程序入口 main 函数实现。
6、config.toml 文件: 配置文件。
7、Makefile 文件: 定义了工程编译,运行,退出的一些命令。
8、go.mod 文件: mod 依赖管理文件。
9、README.md 文件:工程描述文件。
10、.gitignore 文件: 在 git 中忽略的问题件,比如 config.toml 文件本地和服务器上肯定是不一致的,需要忽略。

示例代码

本示例代码中简单模拟实现了账号注册 和 say-hello 的 rest 接口 基于echo搭建的http工程

请求示例:

  • say-hello
    请求
curl -i -L -X GET \
 'http://0.0.0.0:8511/account/say-hello?name=feifeiyu'

返回

{
	"msg": "Hello feifeiyu"
}
  • 用户注册 请求
curl -i -L -X POST \
   -H "Content-Type:application/json" \
   -d \
'{
  "name": "feifeiyu",
  "gender": 1,
  "address": "well street"
}' \
 'http://0.0.0.0:8511/account/register'

返回

{
   "msg": "OK"
}