fastGin脚手架——初始化
项目创建 首先就是初始化部分 我们得创建一个项目,通常是使用goland去建项目 正常情况下,我们是需要去配置一个GOPROXY的环境变量 GOPROXY=https://goproxy.
fastGin脚手架——初始化
发布时间:2025-01-17 (2025-01-17)

项目创建

首先就是初始化部分

我们得创建一个项目,通常是使用goland去建项目

正常情况下,我们是需要去配置一个GOPROXY的环境变量

GOPROXY=https://goproxy.cn

这句话的意思就是配置Go的代理为 https://goproxy.cn,这个是国内的

我们以后下载依赖的时候,就会比较快,并且很容易就下载好了

当然,这一块你这里不填,你直接改环境变量也是一样的

你只要输入 go env发现GOPROXY是这个网址就是正常的

如果是https://proxy.golang.org, 那么可以通过 go env -w GOPROXY=https://goproxy.cn 去修改

配置文件

程序中,一些不经常变化的值,我们会把它存放到配置文件中

比如数据库的地址、用户名密码,jwt的过期时间,文件上传的路径等等

如果不用配置文件,假如你想改某一项配置,那么你的程序就得重新编译

配置文件使用的是yaml文件

你用toml,ini,json也可以,只需要用go去解析对应的文件就行

但是yaml要灵活一点,而且用起来简单,可以写注释

这里需要简单掌握一下yaml的语法,不用掌握很多,会简单的配置就好了

冒号和后面的值之间是用空格的

name: 字符串
name1: '字符串'
name2: "字符串" # 这三种字符串都可以
age: 21
isGender: true
isGender1: True  # 两种bool都可以
arr1: [1,2,3]
arr2:
  - 1
  - 2
  - 3 # 两种数组都可以
obj2: 
  name: fengfeng
  age: 12

解析yaml文件

使用的库是 gopkg.in/yaml.v3

byteData, _ := os.ReadFile("settings.yaml")
var data map[string]any
err := yaml.Unmarshal(byteData, &data)
if err != nil {
  fmt.Println(err)
  return
}
fmt.Println(data)

我这里只是用map去接,当然这里可以改成其他的,比如对应的结构体

配置的引申

这里引申一个知识点

现在如果改了配置文件,那么程序是需要重启才能拿到新的值

有没有什么办法,不重启容器也能动态改配置呢

有两种办法

第一种是直接存内存

第二种是引入配置中心,通过接口去请求对应的配置项,比如etcd、nacos等

日志配置

日志这是一件非常重要的东西

非常建议大家在写项目的时候多打日志

因为我们开发项目的时候,肯定多多少少会有问题,这很正常

重点是,能不能通过日志就能发现问题出在哪里

而不是根据问题去看代码然后去分析原因

然后这个地方也牵扯到了后端报错要不要返回给前端的问题

这种情况得分类讨论,如果是公司内部使用的话,就把错误直接返回就行了,这样以后遇到错误能马上知道错误的原因,然后改掉

但是如果是外部使用,直接返回后端的错误,显得你们这个产品没有水平,统一封装一下,比如网络错误,系统错误,然后在日志里面把具体错误显示出来

日志的选择,使用的是logrus,当然你也可以选择zap

go get github.com/sirupsen/logrus

一般来说,日志打印的时候,要能够知道是那个地方打印的,以及打印的时间

还有一些升级功能,比如日志分片,按时间分,按大小分,按日志级别分等

  1. 配置格式化

需要实现Format(entry *logrus.Entry) ([]byte, error) 方法

type MyLog struct {
}

// 颜色
const (
  red    = 31
  yellow = 33
  blue   = 36
  gray   = 37
)

func (MyLog) Format(entry *logrus.Entry) ([]byte, error) {
  //根据不同的level去展示颜色
  var levelColor int
  switch entry.Level {
  case logrus.DebugLevel, logrus.TraceLevel:
    levelColor = gray
  case logrus.WarnLevel:
    levelColor = yellow
  case logrus.ErrorLevel, logrus.FatalLevel, logrus.PanicLevel:
    levelColor = red
  default:
    levelColor = blue
  }
  var b *bytes.Buffer
  if entry.Buffer != nil {
    b = entry.Buffer
  } else {
    b = &bytes.Buffer{}
  }
  //自定义日期格式
  timestamp := entry.Time.Format("2006-01-02 15:04:05")
  if entry.HasCaller() {
    //自定义文件路径
    funcVal := entry.Caller.Function
    fileVal := fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line)
    //自定义输出格式
    fmt.Fprintf(b, "[%s] \x1b[%dm[%s]\x1b[0m %s %s %s\n", timestamp, levelColor, entry.Level, fileVal, funcVal, entry.Message)
  }
  return b.Bytes(), nil
}
  1. 配置hook

需要实现产生日志之后,把它写入到日志文件中去

按天分片

错误的日志单独存放

type MyHook struct {
  file     *os.File
  errFile  *os.File
  fileDate string
  logPath  string
  mu       sync.Mutex
}

func (hook *MyHook) Fire(entry *logrus.Entry) error {
  hook.mu.Lock()
  defer hook.mu.Unlock()

  timer := entry.Time.Format("2006-01-02")
  line, err := entry.String()
  if err != nil {
    return fmt.Errorf("failed to format log entry: %v", err)
  }

  if hook.fileDate != timer {
    if err := hook.rotateFiles(timer); err != nil {
      return err
    }
  }

  if _, err := hook.file.Write([]byte(line)); err != nil {
    return fmt.Errorf("failed to write to log file: %v", err)
  }

  if entry.Level <= logrus.ErrorLevel {
    if _, err := hook.errFile.Write([]byte(line)); err != nil {
      return fmt.Errorf("failed to write to error log file: %v", err)
    }
  }

  return nil
}

// rotateFiles 日志轮换
func (hook *MyHook) rotateFiles(timer string) error {
  if hook.file != nil {
    if err := hook.file.Close(); err != nil {
      return fmt.Errorf("failed to close log file: %v", err)
    }
  }
  if hook.errFile != nil {
    if err := hook.errFile.Close(); err != nil {
      return fmt.Errorf("failed to close error log file: %v", err)
    }
  }

  dirName := fmt.Sprintf("%s/%s", hook.logPath, timer)
  if err := os.MkdirAll(dirName, os.ModePerm); err != nil {
    return fmt.Errorf("failed to create log directory: %v", err)
  }

  infoFilename := fmt.Sprintf("%s/info.log", dirName)
  errFilename := fmt.Sprintf("%s/err.log", dirName)

  var err error
  hook.file, err = os.OpenFile(infoFilename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
  if err != nil {
    return fmt.Errorf("failed to open log file: %v", err)
  }

  hook.errFile, err = os.OpenFile(errFilename, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0600)
  if err != nil {
    return fmt.Errorf("failed to open error log file: %v", err)
  }

  hook.fileDate = timer
  return nil
}

// Levels 哪些级别的日志能走 Fire 方法
func (hook *MyHook) Levels() []logrus.Level {
  return logrus.AllLevels
}

数据库连接

支持三种数据库,四种连接方式

https://gorm.io/zh_CN/docs/connecting_to_the_database.html#MySQL

  1. mysql

  2. pgsql

  3. sqlite github.com/glebarez/sqlite

    如果这里用默认的gorm.io/driver/sqlite 大概率会触发

    [error] failed to initialize database, got error Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub panic: Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub

    这个错误,原因是 GORM SQLite驱动使用了CGO实现,需要在CGO环境下才能工作。

    参考文档:https://www.jb51.net/jiaoben/3147895tu.htm

  4. 不连

配置文件

db:
  mode: mysql # mysql pgsql sqlite ""
  db_name: ""
  host: ""
  port: 3306
  user: ""
  password: ""

通过mode的不同,调用不同的初始化gormDB方法,我们可以使用简单工厂模式

package core

import (
  "fmt"
  "github.com/glebarez/sqlite"
  "github.com/sirupsen/logrus"
  "gorm.io/driver/mysql"
  "gorm.io/driver/postgres"
  "gorm.io/gorm"
  "time"
)

type DB struct {
  Mode     string `yaml:"mode"`
  DbName   string `yaml:"db_name"`
  Host     string `yaml:"host"`
  Port     int    `yaml:"port"`
  User     string `yaml:"user"`
  Password string `yaml:"password"`
}

func InitGorm() (database *gorm.DB) {
  var db DB
  var dialector gorm.Dialector
  switch db.Mode {
  case "mysql":
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local",
      db.User,
      db.Password,
      db.Host,
      db.Port,
      db.DbName,
    )
    dialector = mysql.Open(dsn)
  case "pgsql":
    dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%d sslmode=disable TimeZone=Asia/Shanghai",
      db.Host,
      db.User,
      db.Password,
      db.DbName,
      db.Port,
    )
    dialector = postgres.Open(dsn)
  case "sqlite":
    dialector = sqlite.Open(db.Host)
  case "":
    logrus.Warnf("未配置数据库连接")
    return nil
  default:
    logrus.Fatalf("不支持的数据库mode配置")
  }
  database, err := gorm.Open(dialector, &gorm.Config{
    DisableForeignKeyConstraintWhenMigrating: true, // 不生成实体外键
  })
  if err != nil {
    logrus.Fatalf("数据库连接失败 %s", err)
    return
  }
  // 配置连接池
  sqlDB, err := database.DB()
  if err != nil {
    logrus.Fatalf("获取数据库连接失败 %s", err)
    return
  }
  err = sqlDB.Ping()
  if err != nil {
    logrus.Fatalf("数据库连接失败 %s", err)
    return
  }
  // 设置连接池
  sqlDB.SetMaxIdleConns(10)
  sqlDB.SetMaxOpenConns(100)
  sqlDB.SetConnMaxLifetime(time.Hour)
  return 
}

正常情况下来说,跑一个项目数据库没有或者数据库没连上,肯定就不用再往下走了

但是为了方便你们以最小成本把这个项目跑起来,所以我允许不去连数据库

但是后续使用的时候,要进行db的为空判断,不然会出现空指针错误

redis连接

这一块没什么好说的,比较简单

package core

import (
  "context"
  "github.com/redis/go-redis/v9"
  "github.com/sirupsen/logrus"
)

type Conf struct {
  Addr     string
  Password string
  DB       int
}

func InitRedis() {
  var conf Conf
  rdb := redis.NewClient(&redis.Options{
    Addr:     conf.Addr,
    Password: conf.Password,
    DB:       conf.DB,
  })

  _, err := rdb.Ping(context.Background()).Result()
  if err != nil {
    logrus.Errorf("连接redis失败 %s", err)
    return
  }
  logrus.Infof("成功连接redis")
}