作者: ethan

  • 从零开始搭建go项目(gin框架)(四) – 支持Redis

    本节内容,我们将redis引入我们的框架 (本地redis部署可参考 Redis Cluster集群部署 无需搭建集群,部署单redis即可)。

    第一步 引入redis服务 – go-redis

    go get github.com/go-redis/redis/v8

    第二步 对go-redis进行封装

    mkdir ./library/cache

    创建文件./library/cache/redis.go:

    package cache
    
    import (
    	"github.com/gin-gonic/gin"
    	"github.com/go-redis/redis/v8"
    	"time"
    )
    
    type RedisClient struct {
    	Client *redis.Client
    	Ctx *gin.Context
    }
    
    type RedisConf struct {
    	Addr         string        `yaml:"addr"`
    	Password     string        `yaml:"password"`
    	PoolSize     int           `yaml:"poolSize"`
    	DialTimeout  time.Duration `yaml:"dialTimeout"`
    	ReadTimeout  time.Duration `yaml:"readTimeout"`
    	WriteTimeout time.Duration `yaml:"writeTimeout"`
    }
    
    var RedisCacheClient *RedisClient
    
    func InitRedisClient(ctx *gin.Context, redisConf *RedisConf) {
    	RedisCacheClient = &RedisClient{Client: redis.NewClient(&redis.Options{
    		Addr: redisConf.Addr,
    		Password: redisConf.Password,
    		PoolSize: redisConf.PoolSize,
    		DialTimeout: redisConf.DialTimeout,
    		ReadTimeout: redisConf.ReadTimeout,
    		WriteTimeout: redisConf.WriteTimeout,
    	}), Ctx: ctx}
    }
    
    func GetRedisClient() *RedisClient {
    	return RedisCacheClient
    }
    
    func (r RedisClient) Set(key string, value interface{}, expiration time.Duration) error {
    	return r.Client.Set(r.Ctx, key, value, expiration).Err()
    }
    
    func (r RedisClient) Get(key string) (string, error) {
    	return r.Client.Get(r.Ctx, key).Result()
    }
    
    func (r RedisClient) Del(key string) error {
    	return r.Client.Del(r.Ctx, key).Err()
    }
    
    func (r RedisClient) SetNX(key string, value interface{}, expiration time.Duration) error {
    	return r.Client.SetNX(r.Ctx, key, value, expiration).Err()
    }
    
    func  (r RedisClient) Expire(key string, expiration time.Duration) error {
    	return r.Client.Expire(r.Ctx, key, expiration).Err()
    }
    
    func  (r RedisClient) Exists(key string) error {
    	return r.Client.Exists(r.Ctx, key).Err()
    }
    
    func (r RedisClient) HSet(key string, values ...interface{}) error {
    	return r.Client.HSet(r.Ctx, key, values ...).Err()
    }
    
    func  (r RedisClient) HGet(key, field string) (string, error) {
    	return r.Client.HGet(r.Ctx, key, field).Result()
    }
    
    func (r RedisClient) MSet(values ...interface{}) error {
    	return r.Client.MSet(r.Ctx, values ...).Err()
    }
    
    func (r RedisClient) MGet(keys ...string) ([]interface{}, error) {
    	return r.Client.MGet(r.Ctx, keys ...).Result()
    }
    
    func (r RedisClient) HSetNX(key, field string, value interface{}) error {
    	return r.Client.HSetNX(r.Ctx, key, field, value).Err()
    }
    
    func  (r RedisClient) HExists(key, field string) error {
    	return r.Client.HExists(r.Ctx, key, field).Err()
    }
    
    func  (r RedisClient) HDel(key, field string) error {
    	return r.Client.HDel(r.Ctx, key, field).Err()
    }
    
    func (r RedisClient) HMSet(key string, values ...interface{}) error {
    	return r.Client.HMSet(r.Ctx, key, values ...).Err()
    }
    
    func (r RedisClient) HMGet(key string, fields ...string) ([]interface{}, error) {
    	return r.Client.HMGet(r.Ctx, key, fields ...).Result()
    }
    
    func  (r RedisClient) IncrBy(key string, value int64) error {
    	return r.Client.IncrBy(r.Ctx, key, value).Err()
    }
    
    func  (r RedisClient) DecrBy(key string, value int64) error {
    	return r.Client.DecrBy(r.Ctx, key, value).Err()
    }
    
    func  (r RedisClient) SAdd(key string, members ...interface{}) error {
    	return r.Client.SAdd(r.Ctx, key, members ...).Err()
    }
    
    func  (r RedisClient) ZRange(key string, start, stop int64) ([]string, error) {
    	return r.Client.ZRange(r.Ctx, key, start, stop).Result()
    }
    
    func  (r RedisClient) ZRangeByScore(key string, opt *redis.ZRangeBy) ([]string, error) {
    	return r.Client.ZRangeByScore(r.Ctx, key, opt).Result()
    }
    
    func  (r RedisClient) ZRangeByScoreWithScores(key string, opt *redis.ZRangeBy) ([]redis.Z, error) {
    	return r.Client.ZRangeByScoreWithScores(r.Ctx, key, opt).Result()
    }
    
    func  (r RedisClient) ZRevRange(key string, start, stop int64) ([]string, error) {
    	return r.Client.ZRevRange(r.Ctx, key, start, stop).Result()
    }
    
    func  (r RedisClient) ZRevRangeByScore(key string, opt *redis.ZRangeBy) ([]string, error) {
    	return r.Client.ZRevRangeByScore(r.Ctx, key, opt).Result()
    }
    
    func  (r RedisClient) ZRevRangeByScoreWithScores(key string, opt *redis.ZRangeBy) ([]redis.Z, error) {
    	return r.Client.ZRevRangeByScoreWithScores(r.Ctx, key, opt).Result()
    }
    
    func  (r RedisClient) ZCard(key string) (int64, error) {
    	return r.Client.ZCard(r.Ctx, key).Result()
    }
    
    func  (r RedisClient) SIsMember(key string, member interface{}) (bool, error) {
    	return r.Client.SIsMember(r.Ctx, key, member).Result()
    }
    
    func  (r RedisClient) LPush(key string, values ...interface{}) error {
    	return r.Client.LPush(r.Ctx, key, values ...).Err()
    }
    
    func  (r RedisClient) RPop(key string) (string, error) {
    	return r.Client.RPop(r.Ctx, key).Result()
    }
    
    func  (r RedisClient) RPush(key string, values ...interface{}) error {
    	return r.Client.LPush(r.Ctx, key, values ...).Err()
    }
    
    func  (r RedisClient) LPop(key string) (string, error) {
    	return r.Client.RPop(r.Ctx, key).Result()
    }
    
    func  (r RedisClient) LRange(key string, start, stop int64) error {
    	return r.Client.LRange(r.Ctx, key, start, stop).Err()
    }

    该文件封装了一些常用的redis方法,便于直接使用

    第三步 增加redis配置与读取

    ./conf/config.yaml, 新增

    log: # 日志配置
      # 日志路径
      dir: "./log"
    
    redis:
      addr: 10.252.187.163:6379
      password: "123456"
      # 连接池大小
      poolSize: 10
      # 连接超时时间
      dialTimeout: 100ms
      # 读超时时间
      readTimeout: 500ms
      # 写超时时间
      writeTimeout: 300ms

    ./conf/config.go

    package conf
    
    import (
    	"fusheng-admin/library/cache"
    	"gopkg.in/yaml.v3"
    	"io/ioutil"
    )
    
    var BaseConf Config
    
    var ConfigFile = "./conf/config.yaml"
    
    type LogConf struct {
    	Dir string `yaml:"dir"`
    }
    
    type Config struct {
    	Log   LogConf                        `yaml:"log"`
    	Redis *cache.RedisConf               `yaml:"redis"`
    }
    
    func InitConf() {
    	if yamlFile, err := ioutil.ReadFile(ConfigFile); err != nil {
    		panic("read conf error: " + err.Error())
    	} else if err = yaml.Unmarshal(yamlFile, &BaseConf); err != nil {
    		panic("conf file unmarshal error: " + err.Error())
    	}
    }
    

    第四步 redis初始化

    创建./helpers目录,存放一些辅助工具函数

    ./helpers

    创建文件./helpers/redis.go:

    package helpers
    
    import (
    	"fusheng-admin/conf"
    	"fusheng-admin/library/cache"
    	"github.com/gin-gonic/gin"
    )
    
    func InitRedis() {
    	redisConf := conf.BaseConf.Redis
    	cache.InitRedisClient(&gin.Context{}, redisConf)
    }
    

    第五步xa0main函数中调用

    ./main.go :

    package main
    
    import (
    	"fusheng-admin/conf"
    	"fusheng-admin/helpers"
    	"fusheng-admin/library/middleware"
    	"fusheng-admin/router"
    	"github.com/gin-gonic/gin"
    )
    
    func main()  {
    	// 1.创建路由
    	r := gin.Default()
    	r.Use(middleware.LoggerToFile())
    
    	// 初始化配置
    	conf.InitConf()
    	helpers.InitRedis()
    
    	// 2.绑定路由规则,执行的函数
    	// gin.Context,封装了request和response
    	router.Http(r)
    
    	// 3.监听端口,默认在8080
    	// Run("里面不指定端口号默认为8088")
    	r.Run(":8088")
    }

    第六步 测试

    创建测试目录 ./test

    创建初始化测试工具 ./test/test_init.go:

    package test
    
    import (
    	"fusheng-admin/conf"
    	"fusheng-admin/helpers"
    )
    
    func init()  {
    
    	conf.ConfigFile = "../conf/config.yaml"
    
    	// 初始化配置
    	conf.InitConf()
    	helpers.InitRedis()
    }
    

    创建redis测试 ./test/redis_test.go:

    package test
    
    import (
    	"fmt"
    	"fusheng-admin/library/cache"
    	"testing"
    	"time"
    )
    
    func TestRedis(t *testing.T)  {
    	text := "上次请求时间为: "
    
    	lastReqKey := "test:last:req:time"
    
    	redisClient := cache.GetRedisClient()
    	val, err := redisClient.Get(lastReqKey)
    	if err != nil {
    		t.Log(fmt.Sprintf("get from redis error. %#v", err.Error()))
    	}
    	text += val
    	err = redisClient.Set(lastReqKey, time.Now().Unix(), 600 * time.Second)
    	if err != nil {
    		t.Log(fmt.Sprintf("set redis error. %#v", err.Error()))
    	}
    
    	t.Log(text)
    }

    执行测试(根目录下执行):

    go test -v test/test_init.go test/redis_test.go

    第一次执行:

    第二次执行:

    测试无误,至此,我们的项目已经支持redis啦 ~

    附录

    https://github.com/go-redis/redis

  • 十二生肖来历

    十二生肖,相信大家都很熟悉,可以说是自古流传。

    今天我们就来讲解一下十二生肖的来历。

    关于生肖纪年的方式在我国历史上早有记载,考古发现的战国末期睡虎地秦简与放马滩秦简,两处都出土了《日书》。

    什么是日书呢?其实啊,日书就是古人从事婚嫁、生子、丧葬、农作、出行等各项活动时选择时日吉凶宜忌的参考之书,其本质上啊就是古代民间选吉时测吉凶用的。

    这两地出土的日书中,都出现了配有干支的十二兽的记载。

    干支,就是天干地支的简称,而天干地支,简化而言,天干就是我们所知的,“甲(jiǎ)、乙(yǐ)、丙(bǐng)、丁(dīng)、戊(wù)、己(jǐ)、庚(gēng)、辛(xīn)、壬(rén)、癸(guǐ)”,称为十天干

    而地支则为“子(zǐ)、丑(chǒu)、寅(yín)、卯(mǎo)、辰(chén)、巳(sì)、午(wǔ)、未(wèi)、申(shēn)、酉(yǒu)、戌(xū)、亥(hài)”称为十二地支。

    日书上所记载的就是是配有十二地支的十二种动物。

    睡虎地秦简与放马滩秦简《日书》

    是不是很熟悉了呢?没错,我们如今的十二生肖也是和十二地支配在一起的。“子-鼠,丑-牛,寅-虎,卯-兔,辰-龙,巳-蛇, 午-马,未-羊,申-猴,酉-鸡,戌-狗,亥-猪”。

    不仅如此,日书上记载的兽名、干支排列上与后世十二生肖总体相似,由此可推测睡虎地秦简与放马滩秦简《日书》中出现的配有干支的十二兽应是后世十二生肖的雏形。

    综上,尽管人们还不能确定十二生肖的确切来历,但至少也是在战国时期就已经存在了,因为它的通俗、方便又具有趣味性,所以一直沿用至今,成为古人留给我们的一种仍有实用价值的宝贵遗产。

  • 从零开始搭建go项目(gin框架)(三) – 引入logrus日志

    上篇文章,我们已经给项目加上了路由,并且走通了接口,这篇文章我们再给项目加上一项必不可少的内容 —— 日志.

    目前go的日志记录,用的最多的还是 logrus, 我们这次就将logrus以一个中间件的方式,引入我们的项目。

    第一步 引入logrus

    logrus项目的git地址: https://github.com/sirupsen/logrus

    go get -u github.com/sirupsen/logrus

    第二步 增加日志配置读取逻辑

    在根目录创建配置文件夹,并创建配置文件config.yaml,以及配置解析文件conf.go

    mkdir conf

    ./conf/config.yaml :

    log: # 日志配置
      # 日志路径
      dir: "./log"

    ./conf/conf.go :

    package conf
    
    import (
    	"gopkg.in/yaml.v3"
    	"io/ioutil"
    )
    
    var BaseConf Config
    
    type LogConf struct {
    	Dir string `yaml:"dir"`
    }
    
    type Config struct {
    	Log LogConf `yaml:"log"`
    }
    
    func InitConf()  {
    	confPath := "./conf/config.yaml"
    	if yamlFile, err := ioutil.ReadFile(confPath); err != nil {
    		panic("read conf error: " + err.Error())
    	} else if err = yaml.Unmarshal(yamlFile, &BaseConf); err != nil {
    		panic("conf file unmarshal error: " + err.Error())
    	}
    }

    注意: ioutil.ReadFile(),可以是相对路径,也可以是绝对路径,但相对路径的话,./ 指的是根目录,跟当前文件所在目录无关

    第四步 初始化配置

    在main函数中,初始化配置 ./main.go :

    package main
    
    import (
    	"fusheng-admin/conf"
    	"fusheng-admin/router"
    	"github.com/gin-gonic/gin"
    )
    
    func main()  {
    	// 1.创建路由
    	r := gin.Default()
    
    	// 初始化配置
    	conf.InitConf()
    
    	// 2.绑定路由规则,执行的函数
    	// gin.Context,封装了request和response
    	router.Http(r)
    
    	// 3.监听端口,默认在8080
    	// Run("里面不指定端口号默认为8088")
    	r.Run(":8088")
    }

    第四步 封装logrus

    创建log封装文件 ./library/log/log.go

    mkdir library/log

    library/log/log.go

    package log
    
    import (
    	"fmt"
    	"github.com/sirupsen/logrus"
    	"os"
    	"path"
    	"fusheng-admin/conf"
    )
    
    func init()  {
    	// 设置日志格式为json格式
    	logrus.SetFormatter(&logrus.JSONFormatter{
    		TimestampFormat: "2006-01-02 15:04:05",
    	})
    	logrus.SetReportCaller(true)
    }
    
    func Debug(fields logrus.Fields, args ...interface{})  {
    	setOutPutFile(logrus.DebugLevel)
    	logrus.WithFields(fields).Debug(args)
    }
    
    func Info(fields logrus.Fields, args ...interface{})  {
    	setOutPutFile(logrus.InfoLevel)
    	logrus.WithFields(fields).Info(args)
    }
    
    func Warn(fields logrus.Fields, args ...interface{})  {
    	setOutPutFile(logrus.WarnLevel)
    	logrus.WithFields(fields).Warn(args)
    }
    
    func Fatal(fields logrus.Fields, args ...interface{})  {
    	setOutPutFile(logrus.FatalLevel)
    	logrus.WithFields(fields).Fatal(args)
    }
    
    func Error(fields logrus.Fields, args ...interface{})  {
    	setOutPutFile(logrus.ErrorLevel)
    	logrus.WithFields(fields).Error(args)
    }
    
    func Panic(fields logrus.Fields, args ...interface{})  {
    	setOutPutFile(logrus.PanicLevel)
    	logrus.WithFields(fields).Panic(args)
    }
    
    func Trace(fields logrus.Fields, args ...interface{})  {
    	setOutPutFile(logrus.TraceLevel)
    	logrus.WithFields(fields).Trace(args)
    }
    
    func setOutPutFile(level logrus.Level) {
    	if _, err := os.Stat(conf.BaseConf.Log.Dir); os.IsNotExist(err) {
    		err = os.MkdirAll(conf.BaseConf.Log.Dir, 0777)
    		if err != nil {
    			panic(fmt.Errorf("create log dir '%s' error: %s", conf.BaseConf.Log.Dir, err))
    		}
    	}
    	name := ""
    	switch level {
    	case logrus.DebugLevel:
    		name = "debug"
    	case logrus.InfoLevel:
    		name = "info"
    	case logrus.WarnLevel:
    		name = "warn"
    	case logrus.FatalLevel:
    		name = "fatal"
    	case logrus.ErrorLevel:
    		name = "error"
    	case logrus.PanicLevel:
    		name = "panic"
    	case logrus.TraceLevel:
    		name = "trace"
    	default:
    		panic(fmt.Errorf("invaild log level error %d", logrus.ErrorLevel))
    	}
    	fileName := path.Join(conf.BaseConf.Log.Dir, name + ".log")
    
    
    	var err error
    	os.Stderr, err = os.OpenFile(fileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
    	if err != nil {
    		fmt.Println("open log file err", err)
    	}
    	logrus.SetOutput(os.Stderr)
    	logrus.SetLevel(level)
    	return
    }

    第五步 创建log中间件

    通过创建log中间件,是框架能够自动的记录请求前后的一些信息,便于日志追查。

    创建中间件文件, ./library/middleware/logger.go

    mkdir library/middleware

    library/middleware/logger.go

    package middleware
    
    import (
    	"bytes"
    	"github.com/gin-gonic/gin"
    	"io/ioutil"
    	"time"
    	"word-dect-go/library/log"
    )
    
    func LoggerToFile() gin.HandlerFunc {
    
    	return func(ctx *gin.Context) {
    		// 开始时间
    		start := time.Now()
    
    		// 请求报文
    		var requestBody []byte
    		if ctx.Request.Body != nil {
    			var err error
    			requestBody, err = ctx.GetRawData()
    			if err != nil {
    				log.Warn(map[string]interface{}{"err": err.Error()}, "get http request body error")
    			}
    			ctx.Request.Body = ioutil.NopCloser(bytes.NewBuffer(requestBody))
    		}
    
    		// 处理请求
    		ctx.Next()
    
    		// 结束时间
    		end := time.Now()
    
    		log.Info(map[string]interface{}{
    			"statusCode": ctx.Writer.Status(),
    			"cost": float64(end.Sub(start).Nanoseconds()/1e4) / 100.0,
    			"clientIp": ctx.ClientIP(),
    			"method": ctx.Request.Method,
    			"uri": ctx.Request.RequestURI,
    		})
    	}
    }

    第六步 引入中间件

    r.Use(middleware.LoggerToFile())

    ./main.go :

    package main
    
    import (
    	"fusheng-admin/conf"
    	"fusheng-admin/library/middleware"
    	"fusheng-admin/router"
    	"github.com/gin-gonic/gin"
    )
    
    func main()  {
    	// 1.创建路由
    	r := gin.Default()
    	r.Use(middleware.LoggerToFile())
    
    	// 初始化配置
    	conf.InitConf()
    
    	// 2.绑定路由规则,执行的函数
    	// gin.Context,封装了request和response
    	router.Http(r)
    
    	// 3.监听端口,默认在8080
    	// Run("里面不指定端口号默认为8088")
    	r.Run(":8088")
    }

    第七步 常规调用

    ./service/user.go : (我们增加个接口参数name, 并将其打印到日志)

    // 请求参数结构体
    type UserRequestParams struct {
    	Name string `json:"name"`
    	Ctx *gin.Context
    }
    
    func UserAdd(param *UserRequestParams, responseBody *library.ResponseBody)  {
    	log.Info(map[string]interface{}{"name": param.Name})
    	return
    }

    第七步 接口调用,查看日志

    curl -H "Content-Type: application/json;" -X POST "http://127.0.0.1:8088/api/v1/useradd" -d '{"name":"user1"}'

    可以看到,根目录下自动创建了 ./log 目录,且生成了日志文件info.log :

    {"file":"/Users/ethanxu/Ethan/Work/other/fusheng-admin/library/log/log.go:26","func":"fusheng-admin/library/log.Info","level":"info","msg":"[]","name":"user1","time":"2021-06-28 20:46:09"}
    {"clientIp":"127.0.0.1","cost":1.3,"file":"/Users/ethanxu/Ethan/Work/other/fusheng-admin/library/log/log.go:26","func":"fusheng-admin/library/log.Info","level":"info","method":"POST","msg":"[]","statusCode":200,"time":"2021-06-28 20:46:09","uri":"/api/v1/useradd"}

    至此,我们的项目就可以方便的使用日志啦!

    ps: 日志相关我们并不需要上传至git,所以可以添加.gitignore文件,这样git就会忽略掉日志文件了。该文件可以自行定义,不需要上传至git的文件/文件夹都可以在这里进行配置。(.gitignore文件本身记得上传至git哦~) (bin 为之前创建的编译后程序存放目录,.idea为所用IDE自动生成的目录)

    .gitignore :

    bin
    log
    .idea

  • go web定义全局变量,常驻内存并定时更新重载

    业务场景,特别是web场景中,经常有配置等相关信息,需要进行加载,如果每次请求都加载一次的话,显然是存在性能损耗的。

    这时,就可以将其存储到全局变量中,在程序编译过程中加载并常驻内存,之后,每次接口请求,直接读取该全局变量,并从内存中获取数据即可。

    定义全局变量

    只需在函数外部定义即可

    var globalN int64

    定义定时重载函数

    func ConfReload()  {
    	ticker := time.NewTicker(10 * time.Second)
    	for  {
    		select {
    		case <- ticker.C:
    			globalN++
    		}
    	}
    }

    利用golang的定时器,来实现定时重载,此处每10s重载一次

    启用重载函数

    go ConfReload()

    在main()函数中以协程的方式调用该重载函数,之所以以协程的方式,是为了避免阻塞,不影响其他正常的请求。

  • 从零开始搭建go项目(gin框架)(二) – 规划路由

    上一节 从零开始搭建go项目(gin框架)(一) 中,我们已经实现了一个go项目的基础搭建,实现了程序可访问,本节,我们继续对该项目进行完善和补充。

    第一步 基础准备

    根目录创建 ./library 目录,作为我们的自有库目录

    mkdir library

    创建错误码定义文件 ./library/error_code.go :

    package library
    
    const (
    	ErrnoSuccess = 0
    	ErrnoError   = 1
    	ErrnoUnknown = 2
    )
    var ErrNoToMsgMap = map[int32]string{
    	ErrnoSuccess: "success",
    	ErrnoError:   "failed",
    	ErrnoUnknown: "unknown",
    }
    func GetErrMsg(errNo int32) string {
    	if errMsg, ok := ErrNoToMsgMap[errNo]; ok {
    		return errMsg
    	}
    	return "unknown error"
    }

    接口返回体定义(responseBody) ./library/response.go :

    package library
    
    import (
    	"encoding/json"
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    type ResponseBody struct {
    	Errno int32 `json:"errno"`
    	Msg string `json:"msg"`
    	Data interface{} `json:"data"`
    }
    
    func NewResponseBody() *ResponseBody {
    	return &ResponseBody{
    		Errno: ErrnoSuccess,
    		Msg: GetErrMsg(ErrnoSuccess),
    		Data: map[string]interface{}{},
    	}
    }
    
    func (res *ResponseBody) SetData(data interface{})  {
    	res.Data = data
    }
    
    func (res *ResponseBody) SetErrNo(errNo int32)  {
    	res.Errno = errNo
    }
    
    func (res *ResponseBody) SetErrMsg(errMsg string)  {
    	res.Msg = errMsg
    }
    
    func RecoverResponse(ctx *gin.Context, responseBody *ResponseBody)  {
    	// panic
    	if err := recover(); err != nil {
    		responseBody.SetErrNo(ErrnoUnknown)
    	}
    	resp, err := json.Marshal(responseBody)
    	if err != nil {
    		ctx.Data(http.StatusOK, "application/json;charset=utf-8", []byte(`{"errno":1,"msg":"unknown"}`))
    	} else {
    		ctx.Data(http.StatusOK, "application/json;charset=utf-8", resp)
    	}
    	return
    }

    第二步 创建接口

    在跟没有了创建 ./service 目录,作为我们的接口业务处理层

    ./service/user.go :

    package service
    
    import (
    	"fusheng-admin/library"
    	"github.com/gin-gonic/gin"
    )
    
    // 请求参数结构体
    type UserRequestParams struct {
    	Ctx *gin.Context
    }
    
    func UserAdd(param *UserRequestParams, responseBody *library.ResponseBody)  {
    	return
    }

    在根目录创建./api/v1目录,作为我们的接口目录:

    mkdir -p api/v1

    创建接口 ./api/v1/user.go :

    package v1
    
    import (
    	"fusheng-admin/library"
    	"fusheng-admin/service"
    	"github.com/gin-gonic/gin"
    )
    
    func UserAdd(ctx *gin.Context)  {
    	responseBody := library.NewResponseBody()
    	defer library.RecoverResponse(ctx, responseBody)
    	param := &service.UserRequestParams{Ctx: ctx}
    	ctx.BindJSON(param)
    	service.UserAdd(param, responseBody)
    }

    第三步 定义接口路由

    根目录创建router目录,目录下创建router.go路由文件

    ./router/router.go :

    package router
    
    import (
    	"fusheng-admin/api/v1"
    	"github.com/gin-gonic/gin"
    )
    
    func Http(router *gin.Engine)  {
    	apiRouter := router.Group("/api/v1/")
    	{
    		apiRouter.POST("/useradd", v1.UserAdd)
    	}
    }

    ps: 此处我们定义了一个接口,http://127.0.0.1:8088/api/v1/useradd, 该接口的处理逻辑为 v1.UserAdd函数

    路由规则可以参考 grouping-routes , v1是v1版本的意思,之所以这样设置,是因为对外提供的接口,后期经常是有版本迭代的需求的,这样的话,方便以后对外提供多个版本的接口。

    第四步 入口接入路由

    修改main.go文件,替换掉原来的路由:

    修改后的main.go

    package main
    
    import (
    	"fusheng-admin/router"
    	"github.com/gin-gonic/gin"
    )
    
    func main()  {
    	// 1.创建路由
    	r := gin.Default()
    
    	// 2.绑定路由规则,执行的函数
    	// gin.Context,封装了request和response
    	router.Http(r)
    
    	// 3.监听端口,默认在8080
    	// Run("里面不指定端口号默认为8088")
    	r.Run(":8088")
    }

    第五步 测试

    因为我们路由中,该接口定义的是只接受POST请求,所以只能post方式才可以请求通接口

    如果想要接受GET请求的话,只需将POST改为GET即可,或者也可改为Any, Any可同时接受get和post请求

    第六步 总结

    至此,一个简单的接口,我们就已经实现了。

    本节中,我们首先是实现了路由控制,以及基础的接口返回以及错误码这几个框架层面上的基础功能模块。这些模块抽象出来后,可以极大的简化我们日后的具体的业务逻辑开发,使得开发时无需再去关心这些。

    其次,大家应该也注意到,在api/v1/user.go中,接受参数时,我们是通过一个参数结构体来接收的:

    param := &service.UserRequestParams{Ctx: ctx}
    ctx.BindJSON(param)
    // 请求参数结构体
    type UserRequestParams struct {
    	Ctx *gin.Context
    }

    这样的做法,一方面是规范了我们的接口参数的字段与类型,另一方面也起到了接口参数检查的作用,同时如果后续调用该参数,可以直接调用,更加的方便。

    第八步 参考文档

    https://github.com/gin-gonic/gin

  • 从零开始搭建go项目(gin框架)(一)

    本文会对一些基础的git操作也进行记录,希望对一些对git不是很了解的同学有所帮助。

    第一步 创建git项目

    git init --bare fusheng-admin.git
    chown -R git:git fusheng-admin.git

    这是因为我是自己搭建的git服务,如果使用的是github等官方服务的话,可以直接页面操作创建项目。

    第二步 本地部署项目

    git clone git@47.104.19.86:/git/repo/fusheng-admin.git

    第三步 创建readme说明文件,提交

    第四步 创建go.mod文件

    go mod init fusheng-admin

    go mod是golang的依赖包管理工具,能够让我们即为方面的引入各类包,且对齐进行相应的管理。

    第五步 引入gin框架

    go get -u github.com/gin-gonic/gin

    从go.mod文件中可以看到,gin框架以及其所需要依赖的一些包都已经引入。

    go.sum 文件是go.mod引入包后生成的伴生文件,正常情况下,我们无需理会。

    第六步 创建入口文件main.go

    package main
    
    import (
    	"github.com/gin-gonic/gin"
    	"net/http"
    )
    
    func main()  {
    	// 1.创建路由
    	r := gin.Default()
    
    	// 2.绑定路由规则,执行的函数
    	// gin.Context,封装了request和response
    	r.GET("/", func(c *gin.Context) {
    		c.String(http.StatusOK, "hello World!")
    	})
    
    	// 3.监听端口,默认在8080
    	// Run("里面不指定端口号默认为8088")
    	r.Run(":8088")
    }

    注意,入口文件名不一定是main.go,也不一定一定要在根目录下,但包名必须是main,且必须包含main()函数

    第七步 启动程序

    若只是开发,调试的话,可直接 go run 来启动程序

    go run main.go

    若是生产环境,则通过go build,生成可执行文件,然后运行可执行文件

    mkdir bin
    go build -o ./bin/fusheng_admin main.go
    ./bin/fusheng_admin

    第八步 访问项目

    程序启动后,即可以通过浏览器访问该项目了

  • Elasticseach 随机抽取记录

    业务场景中,特别是审核相关的一些场景,经常会有对数据进行抽取评估的需求。抽取的规则需要尽可能的平均。

    当数据量小的时候,用mysql的 order by rand() 即可实现,但当数据量非常大的时候,该方法就不是很适用了,会非常的慢。

    此时我们就可以借助其他的一些工具,比如elasticsearch (如果你业务中已经使用了的话,否则可以考虑其他的技术方案)

    如果运用elasticsearch来实现随机抽取的目的呢?跟mysql的 order by rand() 采用一样的逻辑,elasticsearch 也支持自定义的排序规则。

    如下, 设置排序规则为 (doc[‘_id’].value + params.random_salt).hashCode(), params.random_salt 为自己设置的加密盐,如我这里设置的是 random_salt = md5(time())

    {
        "_source":{
            "include":[
                "id"
            ]
        },
        "sort":{
            "_script":{
                "script":{
                    "inline":"(doc['_id'].value + params.random_salt).hashCode()",
                    "params":{
                        "random_salt":"50715bab076c6c3376556c85c8236723"
                    }
                },
                "type":"number",
                "order":"asc"
            }
        },
        "size":10,
        "query":{
            "bool":{
                "filter":[
                    {
                        "range":{
                            "create_time":{
                                "gte":1609731331,
                                "lte":1611135233
                            }
                        }
                    }
                ]
            }
        }
    }

    以上检索条件会查询出满足条件的10条随机记录,这是运用了search, 如果想要抽取特别多的数据的话,scroll也同样适用。当然你也可以选择其他的排序规则,只要确保该规则计算的结果尽可能的随机即可。

    注意:如果是想要多页的获取数据的话,必须保证多页的检索条件的sort是同样的,否则就会出现检索出相同的数据(因为每次都是按照不同的排序规则排序,获取该排序规则上,该页的数据)。当然,如果只是想要抽取少量的数据,无需多页获取的话,也可以直接使用 “script”: “Math.random()” 来获取随机数据。

    {
        "_script":{
            "script":"Math.random()",
            "type":"number",
            "order":"asc"
        }
    }

  • 百度打开非常慢解决

    最近百度搜索打开非常慢,甚至会出现打开失败的情况。现记录下解决方案。

    原理就是,将是百度的公用DNS 180.76.76.76 加入到我们的dns中

  • WordPress更新5.6后,编辑文章页面无法打开解决

    今天对wordpress进行了升级,升级到5.6后,突然发现,无论是新建文章,还是编辑文章,页面都无法打开,且页面上并没有明显的错误提示信息。

    最开始,怀疑是升级后一些插件不兼容,于是将插件禁用后,发现还是无法使用,最终,看到站点健康检查中,建议升级php版本(因为已经升级,所以截图中已经看不到了)

    开始怀疑是否是升级后的wordpress版本不兼容老版本的php, 所以进行了php升级,由 php 7.3 升级至 7.4.14,果然该问题修复:

  • Redis浮生系列:为什么要使用Redis

    Redis可以说是目前而言,应用最为广泛的一种nosql数据库,其广受欢迎必然有着其独有的优势。

    优势一:速度快

    redis为什么速度快呢?

    最主要的原因就是redis是基于内存的。

    Redis将所有的数据都放在内存中进行操作,纯内存访问,没有磁盘的I/O读取,自然就非常的快了。

    redis内存读取

    其次,redis是单线程的。

    采用异步非阻塞的多路 I/O 复用,让单个线程高效的处理多个连接请求,减少了网络 IO 的时间消耗,且不存在多线程中线程切换竞争等问题造成的性能损耗。(注意:单线程仅仅指的是网络请求这一模块上用的单线程,整个redis的服务并不是单线程的)

    另外还有就是用最贴近操作系统的C语言编写

    以及优秀的数据结构和算法等也为速度做出了一定的贡献。

    当然,最主要的原因还是基于内存这一点,其提升的速度跟其他因素相比不是一个量级的。

    优势二:丰富的数据类型

    redis提供了五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。

    redis数据类型

    丰富的数据类型不仅能够满足大部分的业务场景需求,同时也极大的提高了业务开发的效率。

    同时redis还提供了 Bitmaps(位图)HyperLogLog(去重计数)GEO(地理信息定位)Bloom Filter(布隆过滤器)PubSub(发布/订阅),Pipeline (管道)事务Lua脚本支持等多项高级功能,能够完美的支持一些特定的应用场景。

    优势三:高可用和分布式

    redis的集群功能是它真正的分布式实现,能够保证Redis节点的鼓掌发现和自动转移,提供了高可用、读写和容量的扩展性,使其能够适应当今主流的分布式业务部署方案。

    正因为有着以上的种种优势,redis才会在目前各大互联网公司广受欢迎,同时也是我们所必须掌握的技术之一。