「Go」Go-搭建IM即时通讯系列文章1-搭建基础框架

通过「Go」Go 组件系列文章,我们已经了解了一些组件的基本使用。下面我们将以实现登录功能为目标来完整的搭建一个基础框架。

和上述步骤一样,我们从配置项搭建开始,在 项目根目录/src/main/ 目录下新建一个 config 目录,用于存放配置文件。在该目录下新建 databasesettings 目录,并分别新建 database.gosettings.go 文件,用做初始化读取配置(viper)以及初始化数据库操作(grom)。

Viper 读取配置

settings.go 提供了读取配置并设置到全局实体提供给其他类使用,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// Package settings
/**
此文件用于读取配置文件 app.yml,并设置到对应实体,以提供给其他文件使用。
因此该文件需要优先进行初始化操作
*/
package settings

import (
"fmt"
"github.com/spf13/viper"
"time"
)

// 定义实体装载配置文件内容

// Settings 设置
type Settings struct {
Server Server `mapstructure:"server"`
Database Database `mapstructure:"db"`
}

var Setting = &Settings{}

// Server 服务
type Server struct {
// Url 地址
Url string `mapstructure:"url"`
// Port 端口
Port int `mapstructure:"port"`
// ReadTimeout 读取超时
ReadTimeout time.Duration `mapstructure:"readTimeout"`
// WriteTimeout 写入超时
WriteTimeout time.Duration `mapstructure:"writeTimeout"`
}

var ServerSetting = &Server{}

// Database 数据库
type Database struct {
// Type 类型
Type string `mapstructure:"type"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Host string `mapstructure:"host"`
Name string `mapstructure:"name"`
TablePrefix string `mapstructure:"prefix"`
}

var DatabaseSetting = &Database{}

// Setup 设置
func Setup() {
// 配置文件名(不带扩展名,即 app.yml 只需要app这部分)
viper.SetConfigName("app")
// 如果配置文件名称中没有扩展名,则为必填项
viper.SetConfigType("yaml")
// 在其中查找配置文件的路径
viper.AddConfigPath("src/resource/")
// 查找并读取配置文件
err := viper.ReadInConfig()
if err != nil {
// 处理读取配置文件时出现的错误
panic(fmt.Errorf("读取配置异常: %w", err))
}
fmt.Println("初始化配置文件成功")
viper.WatchConfig()

// 将配置信息解析为实体
err = viper.UnmarshalKey("settings", Setting)
if err != nil {
panic(fmt.Errorf("读取配置异常,解析失败: %w", err))
}

// 设置为全局变量,后续有其他配置则新增实体和变量即可
ServerSetting = &Setting.Server
DatabaseSetting = &Setting.Database

// 设置初始值
// 超时时间单位设置为秒
ServerSetting.ReadTimeout = ServerSetting.ReadTimeout * time.Second
ServerSetting.WriteTimeout = ServerSetting.WriteTimeout * time.Second
}

该类主要操作

  1. 读取配置文件并解析为实体
  2. 设置全局变量提供给其他类使用
  3. 设置初始值

Gorm 连接数据库

database.go 提供了初始化数据库连接的操作,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package database

import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"log"
"os"
"star-im/src/main/config/settings"
"time"
)

// DBS 定义全局变量,提供给其他方法调用
var DBS *gorm.DB

// Setup 初始化数据库连接
// https://gorm.io/zh_CN/
func Setup() {

var err error
//定义连接路径
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True&loc=Local",
settings.DatabaseSetting.User,
settings.DatabaseSetting.Password,
settings.DatabaseSetting.Host,
settings.DatabaseSetting.Name)

// 连接数据库,并设置基本的配置
// 日志
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer(日志输出的目标,前缀和日志包含的内容)
logger.Config{
// 慢 SQL 阈值
SlowThreshold: time.Second,
// 日志级别
LogLevel: logger.Silent,
// 忽略ErrRecordNotFound(记录未找到)错误
IgnoreRecordNotFoundError: true,
// 彩色打印
Colorful: true,
},
)

DBS, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger,
})

if err != nil {
panic(fmt.Errorf("初始化数据库异常: %w", err))
}

// 获取通用数据库对象 sql.DB ,然后使用其提供的功能
sqlDB, err := DBS.DB()

// 用于设置连接池中空闲连接的最大数量。
sqlDB.SetMaxIdleConns(10)
// 设置打开数据库连接的最大数量。
sqlDB.SetMaxOpenConns(100)

// 设置了连接可复用的最大时间。
sqlDB.SetConnMaxLifetime(time.Hour)
}

该类主要操作

  1. 读取数据库连接配置
  2. 初始化数据库连接
  3. 定义了 慢 SQL 日志配置
  4. 设置了数据库连接池配置
  5. 返回全局变量DBS供其他类使用

Init 加载配置

项目根目录/src/main/config 目录下新建 init.go 文件,用于初始化上面两个配置项。

init.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package config

import (
"star-im/src/main/config/database"
"star-im/src/main/config/redis"
"star-im/src/main/config/settings"
)

// Initial 初始化
func Initial() {
// 初始化配置
settings.Setup()
// 初始化数据库连接
database.Setup()
// 后续有其他配置项可以在下面添加……
}

该文件到时候放在main方法中执行即可

Main 程序入口

项目根目录 下新建一个 main.go 作为我们作为http程序主入口,参考 gin 章节初始化gin

main.go 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
"fmt"
"log"
"net/http"
"star-im/src/main/config"
"star-im/src/main/config/settings"
"star-im/src/main/routers"
)

// init 初始化
func init() {
// 初始化配置项
config.Initial()
}

func main() {
// 路由
routersInit := routers.Setup()
// 读取超时
readTimeout := settings.ServerSetting.ReadTimeout
// 写入超时
writeTimeout := settings.ServerSetting.WriteTimeout
// 端口
endPoint := fmt.Sprintf(":%d", settings.ServerSetting.Port)
// 最大 header 数
maxHeaderBytes := 1 << 20

// 配置 http Server
server := &http.Server{
Addr: endPoint,
Handler: routersInit,
ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
MaxHeaderBytes: maxHeaderBytes,
}

log.Printf("[info] 启动http服务器侦听 %s", endPoint)

// 启动服务
err := server.ListenAndServe()
if err != nil {
// 启动异常
panic(fmt.Errorf("启动服务异常:%w", err))
}
}

该类主要操作

  1. 初始化配置
  2. 初始化路由配置以及服务基础设置

routers.Setup() ,路由等信息单独放在另外一个目录 routers中来统一管理。

router 路由配置

项目根目录/src/main/ 目录下新建一个 routers 目录,并按照层级建立 api/v1 两个目录,用于存放路由接口。v1
表示接口版本号,方便后续迭代接口版本。

项目根目录/src/main/routers/api 目录下创建 health.go ,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package api

import (
"github.com/gin-gonic/gin"
"star-im/src/main/common/app"
)

// Ping 接口连通性测试
func Ping(c *gin.Context) {
// 直接返回成功结果
c.JSON(http.StatusOK, gin.H{
"msg": "成功",
})
}

该类主要做连通性测试,因此直接返回json成功数据

项目根目录/src/main/routers 目录下创建 router.go 文件用于初始化路由配置,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package routers

import (
"github.com/gin-gonic/gin"
"io"
"os"
"star-im/src/main/handler"
"star-im/src/main/routers/api"
)

// Setup 初始化路由
func Setup() *gin.Engine {
r := gin.Default()
// 记录到文件
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)

// 使用中间件
// 统一日志
r.Use(gin.Logger())

// 不需要鉴权
r.GET("/ping", api.Ping)

return r
}

该类主要操作

  1. 初始化路由设置
  2. 指定记录日志到文件
  3. 指定具体的路由地址以及请求方式和响应函数

这时候我们启动根目录下的 main函数即可启动服务,通过浏览器访问:http://localhost:8081/ping 可以得到返回值

1
{"msg": "成功"}

到这一步我们已经能够提供一个提供基础访问的应用,后续我们继续完善应用的内容。