亲手写一个gin server项目-1项目启动
序
go的web框架很多,单体的,微服务的,admin的,工具类的层出不穷。
其实干web开发搞个几年,人人都可以写出一个框架,前提是整个web服务前端后端都搞过一遍,明白其中各种原理。要是天天crud,不在我说的人人里。
框架的好坏不仅仅是性能高,更重要的是面对开发者友好,面对开发者友好我觉的有两个方面,一方面不过度封装,想要实现什么功能,马上能知道如何实现,而不是去翻源码,翻文档。另一方面,在项目需要修改的紧急情况下,能马上定位到代码并顺利进行修改。
面对框架开发这事,我也有过一段惨痛经历,深知简单比复杂还要困难。而我的水平远远没有达到“最简”。
标题之所以不叫“框架”,而是叫项目,是因为我接下来要输出的是一个admin项目,通过这个项目,把我对http server服务的整个流程所涉及到的思考过的知识点同大家讨论讨论。
整个项目包括:项目结构,项目启动,配置文件,中间件,日志,格式化响应,表单校验,检索,jwt会话,rbac鉴权,单元测试,文档生成,基于es的单体日志采集,基于prometheus的指标监控......。
所涉及到的技术栈包括:gin、zap、mysql、gorm、redis、es、prometheus 等。
都是一些常用的技术或者框架,整个项目也都是对各种组件或框架的组合及应用,能使用原生框架api的绝不进行封装,就算封装也是白话封装,因为太高级的封装甚至修改框架,我驾驭不了 ^_^。但是使用效果还行。
如果有精力后边还会在输出一些配套前端内容,主要基于vue3 vben。
因为创作都是在下班后赶工出来的,难免有所不足甚至错误,欢迎大家多多留言指正。
http server服务
web开发其实就是开发一个http协议的服务器。能实现http服务器的技术很多,如python,一般都是前边开个nginx,通过uwsig技术把python脚本挂载在nginx后边。如golang,自己就能实现一个http server服务器。对于请求流直接进行处理响应。(因为只用过这两个,所以没有其他举例了。)
请求流底层都是基于tcp,在上层就是http或者https协议数据了。每次前端请求服务器,服务器对请求流进行处理,然后进行响应。编写的程序就是对这个请求流进行处理并响应了。最长见的的处理就是数据库增删查改。
当然,关于http协议解析,完全没必要自己处理甚至都不用去理解,现成的框架都已经封装做好了,比如我主要用的,大名鼎鼎的go框架,gin。
gin实现一个简单服务器,代码如下:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"Example": "Hello Gin",
})
})
r.Run(":8000")
}
获取参数
项目启动都是一条命令即可,比如上边的那个代码,直接golang run main.go
就可以启动一个服务器,监听8000端口的server服务器。
要是监听不是8000端口呢?总需要外部传一些参数对程序进行控制吧。golang中这类库最著名的就是cobra。
但是我这只是一个http server,明明一条命令能启动的东西,非要上个高大上的东西,有点不合适。
用最简单的获取参数方案就可以,官方库flag。
我个人认为像工具类的应用需要各种参数的项目用cobra才是用对了地方,另外ui类的应用没法用cobra库,曾经我大汗淋淋的把一个cobra项目加了一个ui壳,结果一运行,提示只支持terminal应用,然后又费了半天劲把cobra去掉了。
另一种获取参数方法是
os.Arg
cmder := os.Args[1]
用flag和os.Args 改造一下代码如下:
var port = flag.Int("p", 0, "端口")
func main() {
flag.Usage = Usage
flag.Parse()
if *port == 0 {
flag.Usage()
os.Exit(1)
}
if len(os.Args) < 2 {
flag.Usage()
os.Exit(1)
}
cmder := os.Args[1]
switch cmder {
case "run":
server()
default:
flag.Usage()
os.Exit(1)
}
}
func server() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"Example": "Hello Gin",
})
})
r.Run(":" + strconv.Itoa(*port))
}
func Usage() {
fmt.Println("http server v1.0")
fmt.Println("main run -p 8000")
fmt.Println()
fmt.Println("参数:")
flag.PrintDefaults()
}
context
一个http服务会依赖其他服务,比如mysql、redis等。要想使用这些服务,必须建立对应的cli端,甚至维护一个全生命周期的连接池。
如何才能方便的初始化这些服务,如何才能快速使用这些服务?
一般有两种方案。
- 建立全局对象,创建初始化函数,程序启动时候,调用该函数并实例化该对象,在其他地方都可以导入该对象进行使用。
- 创建一个context对象,把所有资源对象都放在该context对象里,初始化该context对象时候,即把所有资源对象进行初始化,然后把这个context对象注入到所有要使用的地方。
我开始时候就是用的第一种方案,开发时候没问题,就是后边要修改代码时候有点头疼,由于这些初始化函数是散落在项目各个地方,控制其顺序启动就要在搞一套东西,有点复杂,加上时间长了,后边找起来就不是那么方便了。
后来看了go-zero的源码,这些资源东西完全可以在同一个地方用同一个对象进行管理啊。于是有了下边的context对象。
type ServerContext struct {
}
func NewServerContext() (*ServerContext, error) {
return sc, nil
}
目前阶段没有讲到config mysql redis log这些东西,所以这里只有个“空壳”。
这个context和golang中的context还不一样,和gin的gin.Context也不一样,这里这个context只是放置项目中用到的一些资源及初始化用的。
之所以没有放在golang的context和gin.Context里是因为使用时候需要从那些地方拿东西都是interface,需要进行类型转换,到处进行类型转换不是我的风格,所以就干脆弄个定制context,直接拿资源使用。
应用代码如下:
var port = flag.Int("p", 0, "端口")
func main() {
//参数处理
flag.Usage = Usage
flag.Parse()
if *port == 0 {
flag.Usage()
os.Exit(1)
}
if len(os.Args) < 2 {
flag.Usage()
os.Exit(1)
}
cmder := os.Args[1]
switch cmder {
case "run":
server()
default:
flag.Usage()
os.Exit(1)
}
}
//资源上下文
type ServerContext struct {
Src1 string //演示资源
}
func NewServerContext() *ServerContext {
return &ServerContext{
Src1: "这是资源1",
}
}
func server() {
r := gin.Default()
svc := NewServerContext()
r.GET("/", Handler(svc))
r.Run(":" + strconv.Itoa(*port))
}
//资源context注入handler中
func Handler(svc *ServerContext) gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(200, gin.H{
"Example": "Hello Gin",
"src": svc.Src1,
})
}
}
func Usage() {
fmt.Println("http server v1.0")
fmt.Println("main run -p 8000")
fmt.Println()
fmt.Println("参数:")
flag.PrintDefaults()
}
结语
目前说到的参数获取和资源context只是很简单的东西,但是可以在很多程序中应用,不单单是http server。
明天抽个时间把配置文件再写写。
转载自:https://juejin.cn/post/7169615495090405413