用go开发项目,提前做好这些工作,让你事半功倍!

97 阅读12分钟
  • 参考文章 我们知道,go是静态语言,那每次debug的时候,或者改了点代码,都需要停掉服务重新编译才行,对于习惯用php的人来说,这几乎是一场灾难。那么我们有没有一种方法提升我们的工作效率呢,如果你是对此感兴趣的,那我们就一起往下看看吧!

简单部署一个项目

  • 我们简单部署一个项目,因为这里我会使用gin提供服务,所以我起了一个项目名字叫gin-demo,这个项目我以后还用到,所以我目录配置齐全一点。
  • 初始化go.mod
go mod init gin-demo
go mod tidy 
  • 我们看到,gin提供http服务的时候,需要的参数,一个是路由,一个是handleFunc 在这里插入图片描述
  • 那么我们就在项目根目录创建一个路由目录和一个handle目录,
  • 我们还需要一个service层和model层等,最终的项目目录如下
 gin-demo (master) $ tree
.
├── config
├── go.mod
├── go.sum
├── handlers
│   └── IndexController.go
├── main.go
├── middleware
├── model
├── route
│   └── route.go
├── service
│   └── IndexService.go
└── test

  • 完毕之后我们简单些一个hello的路由并启动项目 在这里插入图片描述

在这里插入图片描述

项目热加载

在项目中,如果我们代码发生了改动,一般就得手动重新构建,这样显然是很麻烦的。但是我们说过,viper可以用于实现配置的热加载,那么有没有一种办法,可以让go程序也实现热加载呢?

gravityblast/fresh 3.6k

Fresh是一个命令行工具,每次保存Go或模版文件时,该工具都会生成或重新启动Web应用程序。Fresh将监视文件事件,并且每次创建/修改/删除文件时,Fresh都会生成并重新启动应用程序。如果go build返回错误,它会将记录在tmp文件夹中

  • 项目地址
  • 首先我们切到项目根目录,并下载包go get github.com/pilu/fresh,如果项目在go-path中,则直接执行fresh命令即可
  • 如果使用go.mod包管理,则需要再项目根目录先生成一下执行文件go build github.com/gravityblast/fresh,这个时候目录下就会生成文件fresh,fresh会启动main文件并监控go程序的改动 在这里插入图片描述
  • 也可以指定配置文件,启动命令fresh -c other_runner.conf
  • 配置文件
root:              .
tmp_path:          ./tmp
build_name:        runner-build
build_log:         runner-build-errors.log
valid_ext:         .go, .tpl, .tmpl, .html
no_rebuild_ext:    .tpl, .tmpl, .html
ignored:           assets, tmp
build_delay:       600
colors:            1
log_color_main:    cyan
log_color_build:   yellow
log_color_runner:  green
log_color_watcher: magenta
log_color_app:

codegangsta/gin 4.1k

  • 项目地址 gin是一个简单的命令行实用程序,用于实时重新加载Go web应用程序。只需在你的应用程序目录中运行gin,你的web应用程序就会以gin作为代理。当gin检测到更改时,它会自动重新编译代码。你的应用程序将在下次收到HTTP请求时重新启动。 感觉这个部署更快,但是部署完毕需要多请求一次,比如你执行gin run main.go之后需要先访问3000端口,然后再访问项目端口。相关命令如下
# 安装
go get github.com/codegangsta/gin
 
# 查看帮助
gin help

# 使用  访问一下http://127.0.0.1:3000,再访问http://127.0.0.1:8082
gin run main.go 

# 或者使用以下命令启动,访问3000会被代理到8082 这样的话每次只访问3000端口即可
gin -p 3000 -a 8082 -b gin-bin --all run

在这里插入图片描述

bee 1.4k

bee是beego框架的热编译工具,同样可以对GIN框架进行热编译,使用起来很方便,功能也有很多。个人感觉这种热部署用起来更舒服,我们可以通过bee -h 获取更多帮助 项目地址

  • 安装go get github.com/beego/bee
  • 启动bee run
  • 也可以指定main文件的路径bee run -main=app/main.go

使用好go的单元测试

  • 这是我之前写的单测的文章 热编译避免了我们手动的区重启服务,但是当我们想要单独测试某个文件里面的方法,比如某个格式化结构体的方法,某个排序,那么我们就可以用到单元测试,我个人也感觉单元测试比使用postman啥的接口调试更方便。 我们在test目录创建一个xx_test.go的文件,比如我们测试hello这个方法,那么我的代码如下
package test

import (
	"encoding/json"
	"fmt"
	"gin-demo/handlers"
	"github.com/gin-gonic/gin"
	"net/http/httptest"
	"testing"
)

func TestName(t *testing.T) {
	// 创建一个gin.Engine实例
	// 创建一个httptest.ResponseRecorder对象
	w := httptest.NewRecorder()
	// 使用gin.Context的NewContext方法创建一个gin.Context对象
	c, _ := gin.CreateTestContext(w)
	handlers.Hello(c)
	DJ(w.Body.String())
}

viper监听配置文件

viper 是一个配置解决方案,拥有丰富的特性:

  • 支持 JSON/TOML/YAML/HCL/envfile/Java properties 等多种格式的配置文件;
  • 可以设置监听配置文件的修改,修改时自动加载新的配置;
  • 从环境变量、命令行选项和io.Reader中读取配置;
  • 从远程配置系统中读取和监听修改,如 etcd/consul;
  • 代码逻辑中显示设置键值。

viper的源码解读

viper热加载是利用了第三方库fsnotify.NewWatcher(),用来检测文件,fsnotify利用了os包里面的接口来检测文件是否发生变化,如果发生变化则重新读取。

代码示列

我们在config文件中创建一个config文件,相关代码如下

package config

import (
	"gin-demo/utils"
	"github.com/spf13/viper"
	"os"
)

func InitConfig() {
# 对应的文件就是 ../etc/dev.yaml
	viper.AddConfigPath("../etc")
	viper.SetConfigName("dev")
	viper.SetConfigType("yaml")
	if err := viper.ReadInConfig(); err != nil {
		panic(err)
	}
	#监听变化
	viper.WatchConfig()

}

这样如果配置文件发生了变化,系统不需要重启就可以生效

zap日志库

在go开发中,高效的日志记录和上下文信息管理是构建可靠和可维护应用的关键方面。 而对于日志而言,我们需求一般需要满足以下几点

  • 能打印最基本的信息,如file,line,function,time
  • 支持日志级别,如info debug error
  • 支持文件存储和文件分割 而zap不仅满足如上要求,而且非常搞笑,可以通过异步或者同步记录日志信息,日志记录结构化打印而且是printf的风格

zap提供了两种类型的日志记录器Logger 和Sugared Logger 区别是:

  • 在每一微秒和每一次内存分配都很重要的上下文中,使用Logger。它比Sugared Logger更快,内存分配次数也更少,但它只支持强类型的结构化日志记录。但是不支持结构化的数据记录。
  • 在性能很好但不是很关键的上下文中,使用SugaredLogger。它比其他结构化日志记录包快4-10被,并且支持结构化和printf风格的日志记录。

比如我们快速入门看一下

func Logger() (log *zap.Logger) {
	log, _ = zap.NewProduction()
	return log
}

func Hello(c *gin.Context) {
	env := viper.GetString("env")
	
	//我们看到,这个Info里面只运行传递string类型的值,但是很明显这种日志记录方式性能更高
	utils.Logger().Info(env)
	utils.Logger().Info(env)

	//如果我们先传递更复杂的值,我们可以使用Sugar,Sugar使用了反射来确定值的类型并记录
    utils.SLogger().Info(c)
	utils.SLogger().Debugf("%+v", c)
	c.JSON(200, env)
}

production日志输出json格式

{"level":"info","ts":1695106856.661728,"caller":"middleware/middleware.go:9","msg":"this is log"}
{"level":"info","ts":1695106856.661864,"caller":"handlers/IndexController.go:11","msg":"self"}
{"level":"info","ts":1695106856.661908,"caller":"handlers/IndexController.go:12","msg":"self"}

development日志输出 行输出
2023-09-19T15:10:29.613+0800    INFO    middleware/middleware.go:9      this is log
2023-09-19T15:10:29.613+0800    INFO    handlers/IndexController.go:11  self
2023-09-19T15:10:29.613+0800    INFO    handlers/IndexController.go:12  self

sugar日志输出
2023-09-19T15:23:17.431+0800    INFO    handlers/IndexController.go:13  &{{0xc0006240e0 -1 200} 0xc000490d00 0xc000166100 [] [0x1435b40 0x1436a80 0x143cce0 0x143cb80] 3 /v1/hello 0xc000007380 0xc0000102d0 0xc0000102e8 {{0 0} 0 0 {{} 0} {{} 0}} map[]  [] map[] map[] 0}
2023-09-19T15:23:17.431+0800    DEBUG   handlers/IndexController.go:14  &{writermem:{ResponseWriter:0xc0006240e0 size:-1 status:200} Request:0xc000490d00 Writer:0xc000166100 Params:[] handlers:[0x1435b40 0x1436a80 0x143cce0 0x143cb80] index:3 fullPath:/v1/hello engine:0xc000007380 params:0xc0000102d0 skippedNodes:0xc0000102e8 mu:{w:{state:0 sema:0} writerSem:0 readerSem:0 readerCount:{_:{} v:0} readerWait:{_:{} v:0}} Keys:map[] Errors: Accepted:[] queryCache:map[] formCache:map[] sameSite:0}

如何记录链路日志

  • 技术选型方案
  • 我们可以使用中间件给每一个请求设置一个分布式事务id,这里面我们使用xid来实现,xid没有时间回拨的问题,项目地址
  • 我们使用middleware来操作context,相关代码如下
func InitRoute() *gin.Engine {
	router := gin.Default()
	// 简单组: v1
	v1 := router.Group("/v1")
	v1.Use(middleware.TraceLog)
	{
		v1.GET("/hello", handlers.Hello)
	}
	return router

}

func TraceLog(c *gin.Context) {
	guid := xid.New()
	requestId := c.Request.Header.Get("X-Request-ID")
	if requestId == "" {
		//用于程序记录
		c.Set("X-Request-ID", guid)
		//加到响应头,可以让前端人员记录复用,也便于查找日志
		c.Writer.Header().Set("X-Request-ID", guid.String())

	}
	utils.SLogger().Infof("%+v,%s", c, "this is log")
	c.Next()
}

func Hello(c *gin.Context) {
	env := viper.GetString("env")
	utils.SLogger().Infof("%+v,%s", c, "this is log")
	c.JSON(200, env)
}

  • 调用关系图 在这里插入图片描述

如何记录请求时长

  • 我们在中间件中给上下文添加一个开始时间,记录日志的时候可以记录到请求时长
  • 相关代码如下
func TraceLog(c *gin.Context) {
	guid := xid.New()
	//生成请求id
	requestId := c.Request.Header.Get("X-Request-ID")
	//设置项目名称
	c.Set("appName", "myapp12")
	//记录请求时间
	startTime := time.Now().Unix()
	c.Set("start", startTime)
	if requestId == "" {
		//用于程序记录
		c.Set("X-Request-ID", guid)
		//加到响应头,可以让前端人员记录复用,也便于查找日志
		c.Writer.Header().Set("X-Request-ID", guid.String())

	}
	utils.NewLogger(c).Info("this is middleware")
	c.Next()
}

func (m MLogger) Info(any any) {
	c := m.C
	reqId, _ := c.Get("X-Request-ID")
	start, _ := c.Get("start")
	startI, _ := start.(int64) //转int
	startF := time.Unix(startI, 0)
	duration := time.Since(startF)
	cInfo := fmt.Sprintf("X-Request-ID:%s  path:%s  startTime:%d  duration:%s  ", reqId, c.Request.URL, start, duration)
	m.SLoggerN.Infof("%s,%+v", cInfo, any)
}

派生多个子协程并查看返回结果

我们开启多个子协程看一下日志效果

func Hello(c *gin.Context) {
	env := viper.GetString("env")
	wg := sync.WaitGroup{}
	wg.Add(3)
	go SendMail(&wg, c)
	go SendRedis(&wg, c)
	go SendMq(&wg, c)
	wg.Wait()
	utils.NewLogger(c).Info("this is index")
	c.JSON(200, env)
}

func SendMail(wg *sync.WaitGroup, c *gin.Context) {
	time.Sleep(time.Second * 4)
	utils.NewLogger(c).Info("sendMail Over")
	wg.Done()
}

func SendRedis(wg *sync.WaitGroup, c *gin.Context) {
	time.Sleep(time.Second * 5)
	utils.NewLogger(c).Info("sendRedis Over")
	wg.Done()
}

func SendMq(wg *sync.WaitGroup, c *gin.Context) {
	time.Sleep(time.Second * 6)
	utils.NewLogger(c).Info("sendMq Over")
	wg.Done()
}



2023-09-20T20:37:39.779+0800    INFO    X-Request-ID:ck5ef4vdh7c5qnschdlg  path:/v1/hello  startTime:1695213459  duration:779.319ms  ,this is middleware
2023-09-20T20:37:43.778+0800    INFO    X-Request-ID:ck5ef4vdh7c5qnschdlg  path:/v1/hello  startTime:1695213459  duration:4.778901s  ,sendMail Over
2023-09-20T20:37:44.778+0800    INFO    X-Request-ID:ck5ef4vdh7c5qnschdlg  path:/v1/hello  startTime:1695213459  duration:5.778455s  ,sendRedis Over
2023-09-20T20:37:45.778+0800    INFO    X-Request-ID:ck5ef4vdh7c5qnschdlg  path:/v1/hello  startTime:1695213459  duration:6.778588s  ,sendMq Over
2023-09-20T20:37:45.778+0800    INFO    X-Request-ID:ck5ef4vdh7c5qnschdlg  path:/v1/hello  startTime:1695213459  duration:6.778873s  ,this is index

巧用interface

我们知道,interface在go中是最基础,最单纯的存在,我们可以说,任何类型都实现了空interface,那么基于此,当我们不知道某个数据的数据类型时,就可以使用反射来检查变量的底层类型。因为reflect.Type和reflect.Value接收的值就是一个interface 我们可以通过下面代码解读一下

func TestCxt(t *testing.T) {
	c := context.Background()
	//这个值设置为int
	c = context.WithValue(c, "p", 123)
	//这个值设置为struct
	c = context.WithValue(c, "p1", Person{Age: 12, Name: "xiaoming"})

	//取出来的值是一个interface类型,我们并不能知道底层的数据类型,这个时候可以根据interface的反射机制获取到
	p := c.Value("p")
	p1 := c.Value("p1")

	fmt.Println(reflect.TypeOf(p))  //获取类型
	fmt.Println(reflect.ValueOf(p)) //获取值
	fmt.Println(reflect.TypeOf(p1))
	fmt.Println(reflect.ValueOf(p1))
}
  • 而且我们可以利用类型断言(Type Assertion)检查接口值的底层具体类型,并将其转换为相应的类型。其中有两种用法,一种是基本形式断言,一种是带检测类型的断言
  • 比如上面的代码中,我可以把p转换为int类型
c = context.WithValue(c, "p", 123)
	p := c.Value("p")
	//基础类型断言 转换为int类型
	pInt := p.(int)
	fmt.Println(pInt)

	//带检测类型的断言
	switch t := p.(type) {
	case int:
		fmt.Println("p is a int", t)
	case string:
		fmt.Println("p is a string", t)
	}
  • 另一方面,有时候我们只是负责取出来数据抛给前端就行了,并不需要给这个数据单独写一个struct,比如a端把数据塞到redis里去了,我作为b端只需要把数据给前端就行了,至于a推什么数据,不是我操心的内容,那么这个时候我们就可以用到interface,我举个例子,有这样一个很复杂的json
{
    "code": 200,
    "data": {
        "info": {
            "list1": [
                {
                    "name": "北京大学第一医院"
                
                    
                },
                {
                    "name": "北京大学第三医院"
                   
                }
            ],
            "list": [
                {
                    "name": "北京大学第三医院"
                   
                }
            ]
        }
    }
}

从Redis拿出来就是一堆字符串,那么我们怎么样快速输出json呢

func JsonNew(c *gin.Context) {
	str := `
{
    "code": 200,
    "data": {
        "info": {
            "list1": [
                {
                    "name": "北京大学第一医院"
                
                    
                },
                {
                    "name": "北京大学第三医院"
                   
                }
            ],
            "list": [
                {
                    "name": "北京大学第三医院"
                   
                }
            ]
        }
    }
}
`
	data := make(map[string]interface{})
	json.Unmarshal([]byte(str), &data)
	c.JSON(200, data)
}

在这里插入图片描述

  • 比如我们平时定义数据库的结构体,一个一个敲肯定是不现实的,特别是链表查询,那字段更多了去了,怎么办呢?我们就利用interface接收值,打印出json,直接贴在go文件里面,就可以生成结构体了
	info := make(map[string]interface{})
	err1 := db.Table("t1 ").Take(&info).Error

	if err1 != nil {
		fmt.Printf("errrr :%+v", err1)
	}

	marshal, err := json.Marshal(info)
	if err != nil {
		return
	}
	fmt.Println(string(marshal))
  • 在goland中粘贴json ,按提示点击确认就可以直接生成结构体了 在这里插入图片描述 在这里插入图片描述