用gin进行web开发的基本框架搭建

作者很菜,欢迎交流,不对的请指正!

使用gin构建了一个平常开发易用脚手架,代码简洁易读,可快速进行高效web开发。 主要功能有:

  • mysql/redis的配置
  • 使用viper读取配置文件(yaml)
  • 支持swagger文档生成
  • 使用zap作为日志记录,支持日志分割归档

目录结构:
在这里插入图片描述

整合swagger

创建一个docs文件夹,然后获取swagger

go get -u github.com/swaggo/swag/cmd/swag

然后运行下方代码,会获得swagger.json,swagger.yaml,docs.go

swag init

通过gin渲染swagger,在注册路由的地方,绑定gin

import (
	"github.com/gin-gonic/gin"
	_ "go_gateway_back/docs"  // 千万不要忘了导入把你上一步生成的docs

	gs "github.com/swaggo/gin-swagger"
	"github.com/swaggo/gin-swagger/swaggerFiles"
)
swagger的常用注释

main上面

// @title 这里写标题
// @version 1.0
// @description 这里写描述信息
// @termsOfService http://swagger.io/terms/

// @contact.name 这里写联系人信息
// @contact.url http://www.swagger.io/support
// @contact.email support@swagger.io

// @license.name Apache 2.0
// @license.url http://www.apache.org/licenses/LICENSE-2.0.html

// @host 这里写接口服务的host
// @BasePath 这里写base path

controller上面

// GetPostListHandler2 升级版帖子列表接口
// @Summary 升级版帖子列表接口
// @Description 可按社区按时间或分数排序查询帖子列表接口
// @Tags 帖子相关接口
// @Accept application/json
// @Produce application/json
// @Param Authorization header string false "Bearer 用户令牌"
// @Param object query models.ParamPostList false "查询参数"
// @Security ApiKeyAuth
// @Success 200 {object} _ResponsePostList
// @Router /posts2 [get]

对于属性直接写注解即可。

简单的Gin项目框架编写
func InitModule(configPath string, modules []string) error {
	conf := flag.String("config", configPath, "input config file like ./conf/dev/")
	flag.Parse()
	if *conf == "" {
		flag.Usage()
		os.Exit(1)
	}

	log.Println("------------------------------------------------------------------------")
	log.Printf("[INFO]  config=%s\n", *conf)
	log.Printf("[INFO] %s\n", " start loading resources.")

	// 设置ip信息,优先设置便于日志打印
	ips := GetLocalIPs()
	if len(ips) > 0 {
		LocalIP = ips[0]
	}

	// 解析配置文件目录
	if err := ParseConfPath(*conf); err != nil {
		return err
	}

	//初始化配置文件
	if err := InitViperConf(); err != nil {
		return err
	}

	// 加载base配置
	if InArrayString("base", modules) {
		if err := InitBaseConf(GetConfPath("base")); err != nil {
			fmt.Printf("[ERROR] %s%s\n", time.Now().Format(TimeFormat), " InitBaseConf:"+err.Error())
		}
	}

	// 加载redis配置
	if InArrayString("redis", modules) {
		if err := InitRedisConf(GetConfPath("redis_map")); err != nil {
			fmt.Printf("[ERROR] %s%s\n", time.Now().Format(TimeFormat), " InitRedisConf:"+err.Error())
		}
	}

	// 加载mysql配置并初始化实例
	if InArrayString("mysql", modules) {
		if err := InitDBPool(GetConfPath("mysql_map")); err != nil {
			fmt.Printf("[ERROR] %s%s\n", time.Now().Format(TimeFormat), " InitDBPool:"+err.Error())
		}
	}

	// 设置时区
	if location, err := time.LoadLocation(ConfBase.TimeLocation); err != nil {
		return err
	} else {
		TimeLocation = location
	}

	log.Printf("[INFO] %s\n", " success loading resources.")
	log.Println("------------------------------------------------------------------------")
	return nil
}
第一步:使用默认的配置,或是bash界面传入的config路径。
第二步:得到本机的IP,便于日志打印
var LocalIP = net.ParseIP("127.0.0.1")//默认是回环地址


func GetLocalIPs()(ips []net.IP) {
	addrs, err := net.InterfaceAddrs()
	if err != nil{
		return nil
	}
	for _,addr := range addrs{
		ipNet,ok := addr.(*net.IPNet)
		if ok && !ipNet.IP.IsLoopback(){
			if ipNet.IP.To4() != nil {
				ips = append(ips, ipNet.IP)
			}
		}
	}
	return ips
}
第三步:解析配置文件目录,获取配置文件所在文件夹和得知当前的配置环境
func ParseFilePath(config string) error{
	split := strings.Split(config, "/")
	ConfDir = strings.Join(split[:len(split) - 1],"/") //文件夹
	ConfEnv = split[len(split) - 2] // 配置环境
	return nil
}	
第四步:利用viper管理配置文件,通过上面解析出来的文件夹,得到下面所有文件的viper

当我们需要将viper读取的配置反序列到我们定义的结构体变量中时,一定要使用mapstructuretag哦!

var ViperMap map[string]*viper.Viper
func InitViper() error{
	dir, err := os.Open(ConfDir)
	if err != nil{
		return err
	}
	//返回一个最大长度为1024的文件夹下的文件切片
	fileList, err := dir.Readdir(1024)
	if err != nil{
		return err
	}
	for _,f := range fileList{
		if !f.IsDir(){
			//得到了byte切片的文件内容,viper读入需要转换成reader
			content, err := ioutil.ReadFile(ConfDir + "/" + f.Name())
			if err != nil{
				return err
			}
			v := viper.New()
			v.SetConfigType("yaml")
			err = v.ReadConfig(bytes.NewBuffer(content))
			strs := strings.Split(f.Name(),".")
			if ViperMap == nil{
				ViperMap = make(map[string]*viper.Viper)
			}
			ViperMap[strs[0]] = v
		}
	}
	return nil
}
第五步:初始化Base信息(包含调试模式,时区,日志配置)

base.yaml

在这里插入图片描述

日志使用zap日志。

此时我们就要操控viper,进行配置文件的控制了。首先我们不希望一个个getString,getBool这样获得每一个详细的子配置。我们可以把这些子配置集合成一个结构体,直接从结构体获取会好很多,所以我们有这样的方法。

func ParseConfig(path string,p interface{}) error{
	f, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("Open config %v fail, %v", path, err)
	}
	content, err := ioutil.ReadAll(f)
	if err != nil {
		return fmt.Errorf("Read config fail, %v", err)
	}
	v := viper.New()
	v.SetConfigType("yaml")
	v.ReadConfig(bytes.NewBuffer(content))
	if err = v.Unmarshal(p);err != nil{
		return fmt.Errorf("Parse config fail, config:%v, err:%v", string(content), err)
	}
	return nil
}

自定义zap日志:

func GetEncoder() zapcore.Encoder{
	return zapcore.NewJSONEncoder(zapcore.EncoderConfig{
		TimeKey:        "ts",
		LevelKey:       "level",
		NameKey:        "logger",
		CallerKey:      "caller",
		FunctionKey:    zapcore.OmitKey,
		MessageKey:     "msg",
		StacktraceKey:  "stacktrace",
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.CapitalLevelEncoder,
		EncodeTime:     zapcore.ISO8601TimeEncoder,
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder,
	})
}

func GetWriteSyncer(logFile string) zapcore.WriteSyncer{
	path := GetCurrentPath()
	path = path + "/logs/" + logFile
	w, err := os.Open(path)
	if err != nil{
		return nil
	}
	return zapcore.AddSync(w)
}

func GetLogLevel(level string) zapcore.Level{
	switch level {
	case "debug": return zapcore.DebugLevel
	case "info": return zapcore.InfoLevel
	case "error": return zapcore.ErrorLevel
	case "warn": return zapcore.WarnLevel
	default:
		return zapcore.DebugLevel
	}
}

上面这些自定义的日志,还差一种功能,就是日志切割归档功能。为了添加日志切割归档功能,我们将使用第三方库Lumberjack来实现。

func GetWriteSyncer(logFile string) zapcore.WriteSyncer{
	path := GetCurrentPath()
	path = path + "/logs/" + logFile
	logger := &lumberjack.Logger{
		Filename:   path,
		MaxSize:    Conf.Log.MaxSize,
		MaxAge:     Conf.Log.MaxAge,
		MaxBackups: Conf.Log.MaxBackup,
		Compress:   false,
	}
	return zapcore.AddSync(logger)
}

其实我们更希望日志在开发环境时也可以打印在控制台上,对我们开发方便一点。又可以将代码升级为:

可以使用Zap.NewTee同时使用两个核心,一个核心打印到控制台,一个记录到文件!岂不美哉!

func InitBase(path string)error{
	err := ParseConfig(path, Conf)
	if err != nil {
		return err
	}
	level := Conf.Log.Level
	var core zapcore.Core
	if Conf.DebugMode == "DEBUG"{
		//在控制台输入
		consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
		core = zapcore.NewTee(
				zapcore.NewCore(consoleEncoder,zapcore.Lock(os.Stdout),zapcore.DebugLevel),
				zapcore.NewCore(GetEncoder(), GetWriteSyncer(Conf.Log.LogName), GetLogLevel(level)),
		)
	}else{
		core = zapcore.NewCore(GetEncoder(), GetWriteSyncer(Conf.Log.LogName), GetLogLevel(level))
	}
	//需要一个Core,Core需要Encoder,WriteSyncer,LogLevel。
	logger := zap.New(core,zap.AddCaller())
	//替换全局日志
	zap.ReplaceGlobals(logger)
	return nil
}

全部代码:

package sys_init

import (
	"bytes"
	"fmt"
	"github.com/spf13/viper"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"io/ioutil"
	"log"
	"os"
	"strings"
	"github.com/natefinch/lumberjack"
)
var Conf = new(BaseConfig)
type BaseConfig struct {
	DebugMode 	 string		`mapstructure:"debug-mode"`
	TimeLocation string		`mapstructure:"time-location"`
	Log 		 LogConfig	`mapstructure:"log"`
}

type LogConfig struct {
	Level     string `mapstructure:"level"`
	LogName   string `mapstructure:"logName"`
	MaxSize   int    `mapstructure:"max-size"`
	MaxAge    int    `mapstructure:"max-age"`
	MaxBackup int    `mapstructure:"max-backup"`
}
func InitBase(path string)error{
	err := ParseConfig(path, Conf)
	if err != nil {
		return err
	}
	level := Conf.Log.Level
	var core zapcore.Core
	if Conf.DebugMode == "DEBUG"{
		//在控制台输入
		consoleEncoder := zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig())
		core = zapcore.NewTee(
				zapcore.NewCore(consoleEncoder,zapcore.Lock(os.Stdout),zapcore.DebugLevel),
				zapcore.NewCore(GetEncoder(), GetWriteSyncer(Conf.Log.LogName), GetLogLevel(level)),
		)
	}else{
		core = zapcore.NewCore(GetEncoder(), GetWriteSyncer(Conf.Log.LogName), GetLogLevel(level))
	}
	//需要一个Core,Core需要Encoder,WriteSyncer,LogLevel。
	logger := zap.New(core,zap.AddCaller())
	//替换全局日志
	zap.ReplaceGlobals(logger)
	return nil
}



func GetCurrentPath() string {
	dir, err := os.Getwd()
	if err != nil {
		log.Fatal(err)
	}
	return strings.Replace(dir, "\\", "/", -1)
}
func GetEncoder() zapcore.Encoder{
	return zapcore.NewJSONEncoder(zapcore.EncoderConfig{
		TimeKey:        "ts",
		LevelKey:       "level",
		NameKey:        "logger",
		CallerKey:      "caller",
		FunctionKey:    zapcore.OmitKey,
		MessageKey:     "msg",
		StacktraceKey:  "stacktrace",
		LineEnding:     zapcore.DefaultLineEnding,
		EncodeLevel:    zapcore.CapitalLevelEncoder,
		EncodeTime:     zapcore.ISO8601TimeEncoder,
		EncodeDuration: zapcore.SecondsDurationEncoder,
		EncodeCaller:   zapcore.ShortCallerEncoder,
	})
}

func GetWriteSyncer(logFile string) zapcore.WriteSyncer{
	path := GetCurrentPath()
	path = path + "/logs/" + logFile
	logger := &lumberjack.Logger{
		Filename:   path,
		MaxSize:    Conf.Log.MaxSize,
		MaxAge:     Conf.Log.MaxAge,
		MaxBackups: Conf.Log.MaxBackup,
		Compress:   false,
	}
	return zapcore.AddSync(logger)
}

func GetLogLevel(level string) zapcore.Level{
	switch level {
	case "debug": return zapcore.DebugLevel
	case "info": return zapcore.InfoLevel
	case "error": return zapcore.ErrorLevel
	case "warn": return zapcore.WarnLevel
	default:
		return zapcore.DebugLevel
	}
}

func ParseConfig(path string,p interface{}) error{
	f, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("Open config %v fail, %v", path, err)
	}
	content, err := ioutil.ReadAll(f)
	if err != nil {
		return fmt.Errorf("Read config fail, %v", err)
	}
	v := viper.New()
	v.SetConfigType("yaml")
	v.ReadConfig(bytes.NewBuffer(content))
	if err = v.Unmarshal(p);err != nil{
		return fmt.Errorf("Parse config fail, config:%v, err:%v", string(content), err)
	}
	return nil
}

最后就要整合到Gin中!我们看看Gin的Default如何实现的。

在这里插入图片描述

所以我们需要模仿这个,用zap实现Logger和Recovery

//接受gin框架的默认日志
func GinLogger() gin.HandlerFunc {
	return func(c *gin.Context) {
		start := time.Now()
		path := c.Request.URL.Path
		query := c.Request.URL.RawQuery
		c.Next() //执行后面的中间件,然后计算cost
		cost := time.Since(start)
		zap.L().Info(path,
			zap.Int("status", c.Writer.Status()),
			zap.String("method", c.Request.Method),
			zap.String("path", path),
			zap.String("query", query),
			zap.String("ip", c.ClientIP()),
			zap.String("user-agent", c.Request.UserAgent()),
			zap.String("errors", c.Errors.ByType(gin.ErrorTypePrivate).String()),
			zap.Duration("cost", cost))
	}
}

// GinRecovery recover掉项目可能出现的panic,并使用zap记录相关日志
func GinRecovery(stack bool) gin.HandlerFunc {
	return func(c *gin.Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}

				httpRequest, _ := httputil.DumpRequest(c.Request, false)
				if brokenPipe {
					zap.L().Error(c.Request.URL.Path,
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
					return
				}

				if stack {
					zap.L().Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
						zap.String("stack", string(debug.Stack())),
					)
				} else {
					zap.L().Error("[Recovery from panic]",
						zap.Any("error", err),
						zap.String("request", string(httpRequest)),
					)
				}
				c.AbortWithStatus(http.StatusInternalServerError)
			}
		}()
		c.Next()
	}
}
第六步 读取mysql配置

首先看看mysql的配置信息:

在这里插入图片描述

在mysql.go文件中初始化了这几个属性:

var MysqlPoolMap map[string]*sql.DB
var GormPoolMap map[string]*gorm.DB
var MysqlDefaultConn *sql.DB
var GormDefaultConn *gorm.DB

主要就是通过配置文件取到dsn,创建响应的mysql.db和gorm.db,值得注意的是我将Logger配置到了控制台

package sys_init

import (
	"errors"
	"fmt"
	"gorm.io/gorm"
	"database/sql"
	logger2 "gorm.io/gorm/logger"
	"log"
	"os"
	"time"
	"gorm.io/driver/mysql"
)
type MysqlMapConf struct{
	List map[string]*MysqlConf 	`mapstructure:"list"`
}

type MysqlConf struct{
	DriverName 		string 	`mapstructure:"driver-name"`
	DataSourceName 	string 	`mapstructure:"data-source-name"`
	MaxOpenConn		int		`mapstructure:"max-open-conn"`
	MaxIdleConn		int 	`mapstructure:"max-idle-conn"`
	MaxConnLifeTime	int		`mapstructure:"max-conn-life-time"`
}

var MysqlPoolMap map[string]*sql.DB
var GormPoolMap map[string]*gorm.DB
var MysqlDefaultConn *sql.DB
var GormDefaultConn *gorm.DB

func InitMysql(path string)error{
	m := &MysqlMapConf{}
	if err := ParseConfig(path, m);err != nil{
		return err
	}
	if len(m.List) == 0{
		fmt.Printf("[INFO] %s%s\n", time.Now().Format(TimeFormat), " empty mysql config.")
	}
	MysqlPoolMap = make(map[string]*sql.DB)
	GormPoolMap = map[string]*gorm.DB{}
	//mysql的日志
	logger := logger2.New(log.New(os.Stdout,"\r\n",log.LstdFlags),logger2.Config{LogLevel: logger2.Info})
	for k,v := range m.List{
		data, err := sql.Open("mysql", v.DataSourceName)
		if err != nil{
			return err
		}
		data.SetMaxOpenConns(v.MaxOpenConn)
		data.SetMaxIdleConns(v.MaxIdleConn)
		data.SetConnMaxLifetime(time.Duration(v.MaxConnLifeTime ) * time.Second)
		g, err := gorm.Open(mysql.New(mysql.Config{
			Conn: data,
		}), &gorm.Config{Logger: logger})
		if err != nil{
			return err
		}
		MysqlPoolMap[k] = data
		GormPoolMap[k] = g
	}
	if err, db := GetMysqlConn("default");err == nil{
		MysqlDefaultConn = db
	}
	if err, db := GetGormConn("default");err == nil{
		GormDefaultConn = db
	}
	return nil
}

func GetMysqlConn(name string) (error,*sql.DB){
	db,ok := MysqlPoolMap[name]
	if !ok{
		return errors.New("no match mysql connection"),nil
	}
	return nil,db
}
func GetGormConn(name string) (error,*gorm.DB){
	db,ok := GormPoolMap[name]
	if !ok{
		return errors.New("no match gorm connection"),nil
	}
	return nil,db
}
func CloseDB() error {
	for _, dbpool := range MysqlPoolMap {
		dbpool.Close()
	}
	MysqlPoolMap = make(map[string]*sql.DB)
	GormPoolMap = make(map[string]*gorm.DB)
	return nil
}

第七步 读取Redis配置

在这里插入图片描述

读取Redis,相当于暴露读取完了的结构体。

package sys_init

type RedisConfMap struct {
	list	map[string]*RedisConf 	`mapstructure:"redis"`
}

type RedisConf struct {
	ProxyList    []string `mapstructure:"proxy_list"`
	Password     string   `mapstructure:"password"`
	Db           int      `mapstructure:"db"`
	ConnTimeout  int      `mapstructure:"conn_timeout"`
	ReadTimeout  int      `mapstructure:"read_timeout"`
	WriteTimeout int      `mapstructure:"write_timeout"`
}

var ConfRedis *RedisConfMap

func InitRedis(path string)error{
	r := &RedisConfMap{}
	if err := ParseConfig(path, r);err != nil{
		return err
	}
	ConfRedis = r
	return nil
}

RedisFactory

func RedisFactory(name string)(redis.Conn,error){
	if ConfRedis != nil && ConfRedis.list != nil{
		for n,v := range ConfRedis.list{
			if name == n{
				//默认值
				if v.ConnTimeout == 0 {
					v.ConnTimeout = 50
				}
				if v.ReadTimeout == 0 {
					v.ReadTimeout = 100
				}
				if v.WriteTimeout == 0 {
					v.WriteTimeout = 100
				}
				ranHost := v.ProxyList[rand.Intn(len(v.ProxyList))]
				conn, err := redis.Dial(
					"tcp",
					ranHost,
					redis.DialConnectTimeout(time.Duration(v.ConnTimeout)*time.Millisecond),
					redis.DialReadTimeout(time.Duration(v.ReadTimeout)*time.Millisecond),
					redis.DialWriteTimeout(time.Duration(v.WriteTimeout)*time.Millisecond))
				if err != nil{
					return nil,err
				}
				if v.Password != ""{
					_, err := conn.Do("auth", v.Password)
					if err != nil{
						return nil,AuthError
					}
				}
				if v.Db != 0{
					_, err := conn.Do("select", v.Db)
					if err != nil{
						return nil,SelectError
					}
				}
				return conn,nil
			}
		}
	}
	return nil, CreateError
}

在Redis运行时,进行了日志处理

func RedisConfDo(name string, commandName string, args ...interface{}) (interface{}, error) {
	c, err := RedisFactory(name)
	if err != nil {
		zap.L().Sugar().Errorf("_com_redis_failure,method:%v,err:%v,args:%v",commandName,errors.New("RedisConnFactory_error:" + name),args)
		return nil, err
	}
	defer c.Close()

	startExecTime := time.Now()
	reply, err := c.Do(commandName, args...)
	endExecTime := time.Now()
	if err != nil {
		zap.L().Sugar().Errorf("_com_redis_failure,method:%v,err:%v,args:%v,time:%v",commandName,errors.New("RedisConnFactory_error:" + name),args,fmt.Sprintf("%fs", endExecTime.Sub(startExecTime).Seconds()))
	} else {
		replyStr, _ := redis.String(reply, nil)
		zap.L().Sugar().Errorf("_com_redis_success,method:%v,err:%v,args:%v,time:%v,reply:%v",commandName,errors.New("RedisConnFactory_error:" + name),args,fmt.Sprintf("%fs", endExecTime.Sub(startExecTime).Seconds()),replyStr)
	}
	return reply, err
}
开启服务器
路由的初始化(demo)集成swagger
package route

import (
	"github.com/gin-gonic/gin"
	"go_gateway_back/controller"
	_ "go_gateway_back/docs" // 千万不要忘了导入把你上一步生成的docs

	gs "github.com/swaggo/gin-swagger"
	"github.com/swaggo/gin-swagger/swaggerFiles"
)
//参数就相当于中间件,其参数实现了ServeHttp,通过next来到下一个中间件
func InitRoute(middleware ...gin.HandlerFunc) *gin.Engine{
	engine := gin.Default()
	engine.GET("/swagger/*any",gs.WrapHandler(swaggerFiles.Handler))
	engine.GET("/hello",controller.HelloHandler)
	return engine
}

服务器的启动与关闭
package route

import (
	"context"
	"github.com/gin-gonic/gin"
	"go.uber.org/zap"
	"go_gateway_back/sys_init"
	"net/http"
	"time"
)
var HttpServer *http.Server
func StartServer(){
	gin.SetMode(sys_init.Conf.DebugMode)
	route := InitRoute()
	HttpServer = &http.Server{
		Addr:           sys_init.Conf.Addr,
		Handler:        route,
		ReadTimeout:    time.Duration(sys_init.Conf.ReadTimeout) * time.Second,
		WriteTimeout:   time.Duration(sys_init.Conf.ReadTimeout) * time.Second,
		MaxHeaderBytes: 1 << uint(sys_init.Conf.MaxHeaderBytes),
	}
	go func() {
		zap.L().Sugar().Infof(" [INFO] HttpServerRun:%s\n",sys_init.Conf.Addr)
		if err := HttpServer.ListenAndServe();err != nil{
			zap.L().Sugar().Fatalf(" [ERROR] HttpServerRun:%s err:%v\n", sys_init.Conf.Addr, err)
		}
	}()
}
func StopServer(){
	//该方法返回一个Deadline为10s后的ctx
	ctx, cancelFunc := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancelFunc()
	if err := HttpServer.Shutdown(ctx);err != nil{
		zap.L().Sugar().Fatalf(" [ERROR] HttpServerStop err:%v\n", err)
	}
	zap.L().Sugar().Infof(" [INFO] HttpServerStop stopped\n")
}

优雅的关机
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGKILL, syscall.SIGQUIT, syscall.SIGINT, syscall.SIGTERM)
<-quit
上传到gitee

在git bash中写代码

git config --global user.name "user.name"
git config --global user.email "你的邮箱"
ssh-keygen -t rsa -C "你的邮箱"

配置gitee的ssh公钥
在这里插入图片描述

整合goland

在这里插入图片描述

上传到gitee
在这里插入图片描述

那么我的代码上传到了Gitee:

https://gitee.com/sekiro-phm/gin_web_structure

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值