前言
在学习go许久,没看到网上有比较综合的gin web教程,很多都是最基础的教程,完全就是启动个服务返回参数,没有过多的结合实际开发。下面我结合一下我的经验,来写一篇深入的综合教程,包括数据库Mysql、redis结合。
项目结构
初学者一开始很迷茫,究竟项目结构应该是这么样的呢,我的结构仅仅供参考
包名 | 描述 |
---|---|
app | 应用相关的内容,比如request,response的定义 |
config | 配置文件存的地方 |
const | 常量 |
controller | 控制器,需要路由结合 |
file | 控制文件的操作,和日志相关 |
gredis | redis的启动操作 |
logging | 日志相关 |
model | 数据库启动操作,和数据库相关的实体类 |
router | 路由 |
service | 服务逻辑内容 |
setting | 配置相关代码 |
util | 工具类 |
main文件
package main
import (
"github.com/astaxie/beego/logs"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"gotest/src/gredis"
"gotest/src/logging"
"gotest/src/model"
"gotest/src/router"
"gotest/src/setting"
"net/http"
)
func init() {
logs.Info("配置文件设置 初始化配置")
setting.Setup()
logs.Info("数据库 初始化配置")
model.SetUp()
logs.Info("日志文件 初始化配置")
logging.Setup()
logs.Info("redis 初始化配置")
gredis.Setup()
}
func main() {
//debug,release,test
gin.SetMode("debug")
logs.Info("路由 初始化配置")
routersInit := router.InitRouter()
server := &http.Server{
//访问端口
Addr: ":8080",
//路由处理器
Handler: routersInit,
}
//开启服务
server.ListenAndServe()
}
启动程序描述了这几样东西
- 他默认会先执行init()方法,这里面设置了几个启动初始化配置,配置文件、数据库、日志文件、redis,会按顺序启动初始化
- 在main方法中,初始路由配置
- 配置server,输入路由实例和配置的端口
- 启动服务
我下面按顺序来看看这些内容
配置文件初始化
他调用的是setting.Setup(),也就是setting包中的Setup方法,也就是下面这个
package setting
import (
"github.com/go-ini/ini"
"log"
"time"
)
type App struct {
JwtSecret string
PageSize int
PrefixUrl string
RuntimeRootPath string
ImageSavePath string
ImageMaxSize int
ImageAllowExts []string
ExportSavePath string
QrCodeSavePath string
FontSavePath string
LogSavePath string
LogSaveName string
LogFileExt string
TimeFormat string
}
var AppSetting = &App{}
type Server struct {
RunMode string
HttpPort int
ReadTimeout time.Duration
WriteTimeout time.Duration
}
var ServerSetting = &Server{}
type Database struct {
Type string
User string
Password string
Host string
Name string
TablePrefix string
}
var DatabaseSetting = &Database{}
type Redis struct {
Host string
Password string
MaxIdle int
MaxActive int
IdleTimeout time.Duration
}
var RedisSetting = &Redis{}
var cfg *ini.File
// Setup initialize the configuration instance
func Setup() {
var err error
cfg, err = ini.Load("src/config/app.ini")
if err != nil {
log.Fatalf("setting.Setup, fail to parse 'config/app.ini': %v", err)
}
mapTo("app", AppSetting)
mapTo("server", ServerSetting)
mapTo("database", DatabaseSetting)
mapTo("redis", RedisSetting)
AppSetting.ImageMaxSize = AppSetting.ImageMaxSize * 1024 * 1024
ServerSetting.ReadTimeout = ServerSetting.ReadTimeout * time.Second
ServerSetting.WriteTimeout = ServerSetting.WriteTimeout * time.Second
RedisSetting.IdleTimeout = RedisSetting.IdleTimeout * time.Second
}
// mapTo map section
func mapTo(section string, v interface{}) {
err := cfg.Section(section).MapTo(v)
if err != nil {
log.Fatalf("Cfg.MapTo %s err: %v", section, err)
}
}
配置文件放在config包中
[app]
PageSize = 10
JwtSecret = 233
PrefixUrl = http://127.0.0.1:8000
RuntimeRootPath = runtime/
ImageSavePath = upload/images/
# MB
ImageMaxSize = 5
ImageAllowExts = .jpg,.jpeg,.png
ExportSavePath = export/
QrCodeSavePath = qrcode/
FontSavePath = fonts/
LogSavePath = logs/
LogSaveName = log
LogFileExt = log
TimeFormat = 20060102
[server]
#debug or release
RunMode = debug
HttpPort = 8000
ReadTimeout = 60
WriteTimeout = 60
[database]
Type = mysql
User = root
Password = root
Host = 127.0.0.1:3306
Name = blog
TablePrefix = blog_
[redis]
Host = 127.0.0.1:6379
Password =
MaxIdle = 30
MaxActive = 30
IdleTimeout = 200
Setup方法的步骤如下
- 读取src/config/app.ini文件
- 解析出AppSetting
- 解析出ServerSetting
- 解析出DatabaseSetting
- 解析出RedisSetting
- 分别赋值到全局变量,以供外部访问
配置文件初始化结束
数据库初始化配置
这里使用的是mysql,main方法调用的是model.SetUp()
内容如下:
package model
import (
"fmt"
"github.com/go-xorm/xorm"
"gotest/src/setting"
)
type Model struct {
ID int `gorm:"primary_key" json:"id"`
CreatedOn int `json:"created_on"`
ModifiedOn int `json:"modified_on"`
DeletedOn int `json:"deleted_on"`
}
var db *xorm.Engine
func SetUp() {
var err error
db, err = xorm.NewEngine("mysql", fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8",
setting.DatabaseSetting.User,
setting.DatabaseSetting.Password,
setting.DatabaseSetting.Host,
setting.DatabaseSetting.Name))
if err != nil {
panic(err.Error())
}
db.DB().SetMaxIdleConns(10)
db.DB().SetMaxOpenConns(100)
}
日志文件初始化
相关的文件如下:
logging/log.go文件
package logging
import (
"fmt"
"gotest/src/file"
"log"
"os"
"path/filepath"
"runtime"
)
type Level int
var (
F *os.File
DefaultPrefix = ""
DefaultCallerDepth = 2
logger *log.Logger
logPrefix = ""
levelFlags = []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL"}
)
const (
DEBUG Level = iota
INFO
WARNING
ERROR
FATAL
)
// Setup initialize the log instance
func Setup() {
var err error
filePath := getLogFilePath()
fileName := getLogFileName()
F, err = file.MustOpen(fileName, filePath)
if err != nil {
log.Fatalf("logging.Setup err: %v", err)
}
logger = log.New(F, DefaultPrefix, log.LstdFlags)
}
// Debug output logs at debug level
func Debug(v ...interface{}) {
setPrefix(DEBUG)
logger.Println(v)
}
// Info output logs at info level
func Info(v ...interface{}) {
setPrefix(INFO)
logger.Println(v)
}
// Warn output logs at warn level
func Warn(v ...interface{}) {
setPrefix(WARNING)
logger.Println(v)
}
// Error output logs at error level
func Error(v ...interface{}) {
setPrefix(ERROR)
logger.Println(v)
}
// Fatal output logs at fatal level
func Fatal(v ...interface{}) {
setPrefix(FATAL)
logger.Fatalln(v)
}
// setPrefix set the prefix of the log output
func setPrefix(level Level) {
_, file, line, ok := runtime.Caller(DefaultCallerDepth)
if ok {
logPrefix = fmt.Sprintf("[%s][%s:%d]", levelFlags[level], filepath.Base(file), line)
} else {
logPrefix = fmt.Sprintf("[%s]", levelFlags[level])
}
logger.SetPrefix(logPrefix)
}
logging/file.go文件:主要是定义日志文件的输出位置
package logging
import (
"fmt"
"gotest/src/setting"
"time"
)
// getLogFilePath get the log file save path
func getLogFilePath() string {
return fmt.Sprintf("%s%s", setting.AppSetting.RuntimeRootPath, setting.AppSetting.LogSavePath)
}
// getLogFileName get the save name of the log file
func getLogFileName() string {
return fmt.Sprintf("%s%s.%s",
setting.AppSetting.LogSaveName,
time.Now().Format(setting.AppSetting.TimeFormat),
setting.AppSetting.LogFileExt,
)
}
文件相关的操作
file/file.go
package file
import (
"fmt"
"io/ioutil"
"mime/multipart"
"os"
"path"
)
// GetSize get the file size
func GetSize(f multipart.File) (int, error) {
content, err := ioutil.ReadAll(f)
return len(content), err
}
// GetExt get the file ext
func GetExt(fileName string) string {
return path.Ext(fileName)
}
// CheckNotExist check if the file exists
func CheckNotExist(src string) bool {
_, err := os.Stat(src)
return os.IsNotExist(err)
}
// CheckPermission check if the file has permission
func CheckPermission(src string) bool {
_, err := os.Stat(src)
return os.IsPermission(err)
}
// IsNotExistMkDir create a directory if it does not exist
func IsNotExistMkDir(src string) error {
if notExist := CheckNotExist(src); notExist == true {
if err := MkDir(src); err != nil {
return err
}
}
return nil
}
// MkDir create a directory
func MkDir(src string) error {
err := os.MkdirAll(src, os.ModePerm)
if err != nil {
return err
}
return nil
}
// Open a file according to a specific mode
func Open(name string, flag int, perm os.FileMode) (*os.File, error) {
f, err := os.OpenFile(name, flag, perm)
if err != nil {
return nil, err
}
return f, nil
}
// MustOpen maximize trying to open the file
func MustOpen(fileName, filePath string) (*os.File, error) {
dir, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("os.Getwd err: %v", err)
}
src := dir + "/" + filePath
perm := CheckPermission(src)
if perm == true {
return nil, fmt.Errorf("file.CheckPermission Permission denied src: %s", src)
}
err = IsNotExistMkDir(src)
if err != nil {
return nil, fmt.Errorf("file.IsNotExistMkDir src: %s, err: %v", src, err)
}
f, err := Open(src+fileName, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0644)
if err != nil {
return nil, fmt.Errorf("Fail to OpenFile :%v", err)
}
return f, nil
}
redis 初始化
执行的是main方法中的gredis.Setup()
目录中的这里:
gredis/redis.go
package gredis
import (
"encoding/json"
"gotest/src/setting"
"time"
"github.com/gomodule/redigo/redis"
)
var RedisConn *redis.Pool
// Setup Initialize the Redis instance
func Setup() error {
RedisConn = &redis.Pool{
MaxIdle: setting.RedisSetting.MaxIdle,
MaxActive: setting.RedisSetting.MaxActive,
IdleTimeout: setting.RedisSetting.IdleTimeout,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", setting.RedisSetting.Host)
if err != nil {
return nil, err
}
if setting.RedisSetting.Password != "" {
if _, err := c.Do("AUTH", setting.RedisSetting.Password); err != nil {
c.Close()
return nil, err
}
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
return nil
}
// Set a key/value
func Set(key string, data interface{}, time int) error {
conn := RedisConn.Get()
defer conn.Close()
value, err := json.Marshal(data)
if err != nil {
return err
}
_, err = conn.Do("SET", key, value)
if err != nil {
return err
}
_, err = conn.Do("EXPIRE", key, time)
if err != nil {
return err
}
return nil
}
// Exists check a key
func Exists(key string) bool {
conn := RedisConn.Get()
defer conn.Close()
exists, err := redis.Bool(conn.Do("EXISTS", key))
if err != nil {
return false
}
return exists
}
// Get get a key
func Get(key string) ([]byte, error) {
conn := RedisConn.Get()
defer conn.Close()
reply, err := redis.Bytes(conn.Do("GET", key))
if err != nil {
return nil, err
}
return reply, nil
}
// Delete delete a kye
func Delete(key string) (bool, error) {
conn := RedisConn.Get()
defer conn.Close()
return redis.Bool(conn.Do("DEL", key))
}
// LikeDeletes batch delete
func LikeDeletes(key string) error {
conn := RedisConn.Get()
defer conn.Close()
keys, err := redis.Strings(conn.Do("KEYS", "*"+key+"*"))
if err != nil {
return err
}
for _, key := range keys {
_, err = Delete(key)
if err != nil {
return err
}
}
return nil
}
路由初始化
执行的方法是main中的router.InitRouter()
在目录的位置如下:
router/router.go
package router
import (
"github.com/gin-gonic/gin"
"gotest/src/controller"
)
// InitRouter initialize routing information
func InitRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Logger())
r.Use(gin.Recovery())
r.GET("/queryByDatabase", controller.TestQueryByDatabase)
r.GET("/setRedis", controller.TestSetRedis)
return r
}
编写表的实体类,用于类似dao层的处理
编写实体类以及操作
package model
type PersonTable struct {
Id int64 `xorm:"pk autoincr"`
PersonName string `xorm:"varchar(24)"`
PersonAge int `xorm:"int default 0"`
PersonSex int `xorm:"notnull"`
}
func (a *PersonTable) GetAll() ([]PersonTable, error) {
var persons []PersonTable
db.Find(&persons)
return persons, nil
}
对应的sql
CREATE TABLE `person_table` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`person_name` varchar(255) DEFAULT NULL,
`person_age` int(11) DEFAULT NULL,
`person_sex` int(11) DEFAULT NULL
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8mb4;
编写接口控制器controller
目录位置:
controller/testControlelr.go
package controller
import (
"encoding/json"
"fmt"
"github.com/astaxie/beego/logs"
"github.com/gin-gonic/gin"
"gotest/src/app"
"gotest/src/const"
"gotest/src/gredis"
"gotest/src/logging"
"gotest/src/model"
"net/http"
)
func TestQueryByDatabase(c *gin.Context) {
appG := app.Gin{C: c}
personService := model.PersonTable{}
persons, _ := personService.GetAll()
fmt.Println(persons)
appG.Response(http.StatusOK, e.SUCCESS, persons)
}
func TestSetRedis(c *gin.Context) {
appG := app.Gin{C: c}
key := c.Query("key")
value := c.Query("value")
logging.Info("key:", key, " value:", value)
err := gredis.Set(key, value, 1000)
if err != nil {
logs.Error(err)
}
getValue, _ := gredis.Get(key)
var sss string
json.Unmarshal(getValue, &sss)
logs.Info(sss)
appG.Response(http.StatusOK, e.SUCCESS, sss)
}
里面还调用了2个包的内容
如下:
app/response.go
package app
import (
"github.com/gin-gonic/gin"
"gotest/src/const"
)
type Gin struct {
C *gin.Context
}
type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}
// Response setting gin.JSON
func (g *Gin) Response(httpCode, errCode int, data interface{}) {
g.C.JSON(httpCode, Response{
Code: errCode,
Msg: e.GetMsg(errCode),
Data: data,
})
return
}
const/code.go
package e
const (
SUCCESS = 200
ERROR = 500
INVALID_PARAMS = 400
ERROR_EXIST_TAG = 10001
ERROR_EXIST_TAG_FAIL = 10002
ERROR_NOT_EXIST_TAG = 10003
ERROR_GET_TAGS_FAIL = 10004
ERROR_COUNT_TAG_FAIL = 10005
ERROR_ADD_TAG_FAIL = 10006
ERROR_EDIT_TAG_FAIL = 10007
ERROR_DELETE_TAG_FAIL = 10008
ERROR_EXPORT_TAG_FAIL = 10009
ERROR_IMPORT_TAG_FAIL = 10010
ERROR_NOT_EXIST_ARTICLE = 10011
ERROR_CHECK_EXIST_ARTICLE_FAIL = 10012
ERROR_ADD_ARTICLE_FAIL = 10013
ERROR_DELETE_ARTICLE_FAIL = 10014
ERROR_EDIT_ARTICLE_FAIL = 10015
ERROR_COUNT_ARTICLE_FAIL = 10016
ERROR_GET_ARTICLES_FAIL = 10017
ERROR_GET_ARTICLE_FAIL = 10018
ERROR_GEN_ARTICLE_POSTER_FAIL = 10019
ERROR_AUTH_CHECK_TOKEN_FAIL = 20001
ERROR_AUTH_CHECK_TOKEN_TIMEOUT = 20002
ERROR_AUTH_TOKEN = 20003
ERROR_AUTH = 20004
ERROR_UPLOAD_SAVE_IMAGE_FAIL = 30001
ERROR_UPLOAD_CHECK_IMAGE_FAIL = 30002
ERROR_UPLOAD_CHECK_IMAGE_FORMAT = 30003
)
const/msg.go
package e
var MsgFlags = map[int]string{
SUCCESS: "ok",
ERROR: "fail",
INVALID_PARAMS: "请求参数错误",
ERROR_EXIST_TAG: "已存在该标签名称",
ERROR_EXIST_TAG_FAIL: "获取已存在标签失败",
ERROR_NOT_EXIST_TAG: "该标签不存在",
ERROR_GET_TAGS_FAIL: "获取所有标签失败",
ERROR_COUNT_TAG_FAIL: "统计标签失败",
ERROR_ADD_TAG_FAIL: "新增标签失败",
ERROR_EDIT_TAG_FAIL: "修改标签失败",
ERROR_DELETE_TAG_FAIL: "删除标签失败",
ERROR_EXPORT_TAG_FAIL: "导出标签失败",
ERROR_IMPORT_TAG_FAIL: "导入标签失败",
ERROR_NOT_EXIST_ARTICLE: "该文章不存在",
ERROR_ADD_ARTICLE_FAIL: "新增文章失败",
ERROR_DELETE_ARTICLE_FAIL: "删除文章失败",
ERROR_CHECK_EXIST_ARTICLE_FAIL: "检查文章是否存在失败",
ERROR_EDIT_ARTICLE_FAIL: "修改文章失败",
ERROR_COUNT_ARTICLE_FAIL: "统计文章失败",
ERROR_GET_ARTICLES_FAIL: "获取多个文章失败",
ERROR_GET_ARTICLE_FAIL: "获取单个文章失败",
ERROR_GEN_ARTICLE_POSTER_FAIL: "生成文章海报失败",
ERROR_AUTH_CHECK_TOKEN_FAIL: "Token鉴权失败",
ERROR_AUTH_CHECK_TOKEN_TIMEOUT: "Token已超时",
ERROR_AUTH_TOKEN: "Token生成失败",
ERROR_AUTH: "Token错误",
ERROR_UPLOAD_SAVE_IMAGE_FAIL: "保存图片失败",
ERROR_UPLOAD_CHECK_IMAGE_FAIL: "检查图片失败",
ERROR_UPLOAD_CHECK_IMAGE_FORMAT: "校验图片错误,图片格式或大小有问题",
}
// GetMsg get error information based on Code
func GetMsg(code int) string {
msg, ok := MsgFlags[code]
if ok {
return msg
}
return MsgFlags[ERROR]
}
启动
到这里,你可以启动你的go程序,如果你是idea可以直接按main方法的图标启动
验证结果
成功返回正确的数据,从mysql返回正确,从redis设置并返回值,返回正确
总结
我写的这个教程,应该可以满足普通的开发了,本教程结合mysql,redis,log,配置文件,项目结构,都贴合实际开发的场景,如果你想用go-gin来写一个服务,那应该没啥问题,大多服务不就是结合数据库的curd,然后处理后返回数据。如果你正处于迷茫阶段,希望这个教程可以帮助到你,去开启你的go-gin web旅程
年轻人,放手干吧,未来属于你们
just do it