项目创建
首先就是初始化部分
我们得创建一个项目,通常是使用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
一般来说,日志打印的时候,要能够知道是那个地方打印的,以及打印的时间
还有一些升级功能,比如日志分片,按时间分,按大小分,按日志级别分等
- 配置格式化
需要实现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
}
- 配置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
mysql
pgsql
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环境下才能工作。
不连
配置文件
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")
}