一般游戏的角色ID、名字,工会ID、名字,等最好统一创建,方便合服处理,可以以此基础,动态配置生成ID
这个也可以用openresty 作个,可能更专业点,
1:go1.20+ 最后一版支持win7的
mongodb v7.0.9
Redis v=7.2.4
2:代码
3:代码
package main
import (
"fmt"
"generateid/config"
"generateid/httpserver"
"generateid/mongodb"
"generateid/redis"
"github.com/rs/zerolog/log"
"net"
"os"
"os/signal"
"runtime"
"syscall"
)
func main() {
err := config.LoadConfig("config/generateuid.yaml")
if err != nil {
log.Error().Msgf("err: %v", err)
os.Exit(0)
}
if err1 := mongodb.InitMongo(); err1 != nil {
os.Exit(10)
}
if err2 := redis.InitRedis(); err2 != nil {
os.Exit(10)
}
lis := httpserver.SetUpHttpServer(config.GetListenIpPort(), int32(config.GetListenMaxConn()), config.GetMd5Salt(), config.GetServerMOde())
//exit
sysType := runtime.GOOS
if sysType == "windows" {
winCheckExit(lis)
} else if sysType == "linux" {
linuxCheckExit(lis)
}
}
func winCheckExit(lis net.Listener) {
quitchan := make(chan struct{})
go func() {
<-quitchan
fmt.Println("generateid server close")
lis.Close()
}()
fmt.Println("generateid server setup")
for {
var strinput string
fmt.Scan(&strinput)
if strinput == "quit" {
quitchan <- struct{}{}
} else if strinput == "help" {
fmt.Println("input quit to quit progress \n")
}
}
}
func linuxCheckExit(lis net.Listener) {
quitchan := make(chan os.Signal)
signal.Notify(quitchan,
// syscall.SIGINT, //ctrl+c
syscall.SIGINT, //ctrl+/
syscall.SIGTERM,
syscall.SIGHUP, //终端结束
)
exitfun := func() {
fmt.Println("generateid server close")
lis.Close()
//os.Exit(10)
}
for s := range quitchan {
switch s {
case syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: //syscall.SIGHUP,
fmt.Println("Program Exit...", s)
exitfun()
default:
fmt.Println("other signal", s)
}
}
}
package httpserver
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
gdef "generateid/gobaldefine"
"generateid/mongodb"
"go.mongodb.org/mongo-driver/bson"
"log"
"net/http"
"reflect"
"regexp"
"strconv"
"sync/atomic"
"time"
// "sync"
"encoding/json"
"fmt"
redis2 "generateid/redis"
"golang.org/x/net/netutil"
"net"
)
type HttpServer struct {
//curnum int32
maxnum int32
debugmode bool
md5salt string
maxcurrentuserid uint32 //角色当前最大ID
maxcurrentfamilyid uint32 //工会当前最大ID
}
func (self *HttpServer) GetValidUserId() uint32 {
return atomic.AddUint32(&self.maxcurrentuserid, 1)
}
func (self *HttpServer) GetValidFamilyId() uint32 {
return atomic.AddUint32(&self.maxcurrentfamilyid, 1)
}
func (self *HttpServer) SetUserId(uid uint32) {
atomic.StoreUint32(&self.maxcurrentuserid, uid)
}
func (self *HttpServer) SetFamilyId(uid uint32) {
atomic.StoreUint32(&self.maxcurrentfamilyid, uid)
}
var (
httpser HttpServer
)
func GetHttpSerInstance() *HttpServer {
return &httpser
}
const (
SUCESS_GENERATE_NAME = 1 //User_UID 作为name
ERROR_NONE = 0 //没错误 成功的 OK的
ERROR_NAME_INVALIDL = -1 //名字有非法字符或重名或长度不对
ERROR_NAME_DUPLICATE = -2 //重名
ERROR_HANDSHAKE_KEY = -3 //握手码错误
ERROR_TIME_INVALIDL = -4 // 超时
ERROR_OTHER = -15 //其他错误
)
//return map[string]interface{}{
//"uname": name,
//"md5": md5v,
//"sid": sid,
//"st": st,
//}
type JsonGenerateUidReq struct {
UName string `json:"uname"`
Md5v string `json:"md5"`
Sid uint32 `json:"sid"`
Stime int64 `json:"st"`
}
// 统一回复的
type JsonGenerateUidResult struct {
Result int32 `json:"result"` // < 0 错误 -1 名字有非法字符或重名或长度不对 -2 握手码错误 -15 其他错误 >=0 OK 下面的有效
Uid uint32 `json:"uid"`
Name string `json:"name"`
}
//maxcurrentuserid uint32 //角色当前最大ID
//maxcurrentfamilyid uint32//工会当前最大ID
// 得到client ip
func RetmoteIp(r *http.Request) string {
remoteaddr := r.RemoteAddr
if ip := r.Header.Get("Remote_addr"); ip != "" {
remoteaddr = ip
} else {
remoteaddr, _, _ = net.SplitHostPort(remoteaddr)
}
if remoteaddr == "::1" {
remoteaddr = "127.0.0.1"
}
return remoteaddr
}
func getMd5Salt() string {
return httpser.md5salt
}
func isDebugMode() bool {
return httpser.debugmode
}
// 设置跨域
func setkuayu(pw http.ResponseWriter) {
//设置跨域访问 nodejs
/*app.all('*', function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "X-Requested-With");
res.header("Access-Control-Allow-Methods","PUT,POST,GET,DELETE,OPTIONS");
res.header("X-Powered-By",' 3.2.1')
res.header("Content-Type", "application/json;charset=utf-8");
next();
});
*/
//(原因:CORS 头 'Access-Control-Allow-Origin' 不匹配 '*, Content-Type')
pw.Header().Set("Access-Control-Allow-Origin", "*")
pw.Header().Add("Access-Control-Allow-Headers", "Content-Type")
pw.Header().Set("Content-Type", "application/json;charset=utf-8")
}
// 发送json 数据
func sendJson(w http.ResponseWriter, v interface{}) bool {
if data, err := json.Marshal(v); err == nil {
setkuayu(w)
fmt.Fprint(w, string(data))
log.Println(string(data))
return true
} else {
log.Println(err)
log.Println(v)
return false
}
}
func checkstring(str string) bool {
// str := "ABc123汉字./"
//使用regexp.MustCompile函数编译正则表达式^[A-Za-z0-9\u4e00-\u9fa5./]+$,表示字符串只能包含大小写字母、数字、汉字、点和斜杠
//exp := regexp.MustCompile(`^[A-Za-z0-9\u4e00-\u9fa5./]+$`)
//exp := regexp.MustCompile(`^[A-Za-z0-9\u4e00-\u9fa5_-]+$`)
//BASE64URL编码的流程:1、明文使用BASE64进行加密 2、在BASE64的基础上进行一下的编码:2.1)去除尾部的"=" 2.2)把"+"替换成"-" 2.3)把"/"替换成"_"
//BASE64URL解码的流程:1)把"-"替换成"+". 2)把"_"替换成"/" . 3)(计算BASE64URL编码长度)%4 a)结果为0,不做处理 b)结果为2,字符串添加"==" c)结果为3,字符串添加"="
exp := regexp.MustCompile(`^[A-Za-z0-9_-]+$`) //base64url
if exp.MatchString(str) {
//fmt.Println("字符串匹配成功")
return true
} else {
//fmt.Println("字符串匹配失败")
return false
}
}
func GetStringValue(v reflect.Value) string {
switch v.Kind() {
case reflect.String:
return v.String()
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return fmt.Sprintf("%v", v.Uint())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return fmt.Sprintf("%v", v.Int())
}
return ""
}
func geturlparams(r *http.Request) map[string]string {
//Method specifies the HTTP method (GET, POST, PUT, etc.).
params := map[string]string{}
switch r.Method {
case "GET":
{
query := r.URL.Query()
for k, v := range query {
params[k] = v[0]
}
}
case "POST":
{
// 根据请求body创建一个json解析器实例
decoder := json.NewDecoder(r.Body)
// 用于存放参数key=value数据
// 解析参数 存入map
//t := make(map[string]interface{})
// decoder.Decode(¶ms)
ts := &JsonGenerateUidReq{}
if err := decoder.Decode(ts); err == nil {
valueOf := reflect.ValueOf(ts) //.Type()
if valueOf.Kind() == reflect.Pointer || valueOf.Kind() == reflect.Slice {
valueOf = valueOf.Elem()
}
typeOf := reflect.TypeOf(ts)
if typeOf.Kind() == reflect.Pointer || typeOf.Kind() == reflect.Slice {
typeOf = typeOf.Elem()
}
for i := 0; i < valueOf.NumField(); i++ {
fieldvalue := valueOf.Field(i)
fieldtype := typeOf.Field(i)
jsontag := fieldtype.Tag.Get("json")
// log.Printf("k:%v v=%v tag=%v kind=%v v=%v", fieldtype.Name, fieldvalue.Interface(), jsontag, fieldvalue.Kind(), GetStringValue(fieldvalue))
params[jsontag] = GetStringValue(fieldvalue)
}
}
}
}
//return params
return params
}
//func closeconnect(w http.ResponseWriter) {
// hj, ok := w.(http.Hijacker)
// if !ok {
// http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
// return
// }
// conn, bufrw, err := hj.Hijack()
// if err != nil {
// http.Error(w, err.Error(), http.StatusInternalServerError)
// return
// }
// // Don't forget to close the connection:
// defer conn.Close()
// conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
// bufrw.WriteString("Now we're speaking raw TCP. Say hi: ")
// bufrw.Flush()
// s, err := bufrw.ReadString('\n')
// if err != nil {
// log.Printf("error reading string: %v", err)
// return
// }
// fmt.Fprintf(bufrw, "You said: %q\nBye.\n", s)
// bufrw.Flush()
//}
// ServeHTTP implements an http.Handler that answers RPC requests.
//func (server *server) servehttp(w http.responsewriter, req *http.request) {
// if req.method != "connect" {
// w.header().set("content-type", "text/plain; charset=utf-8")
// w.writeheader(http.statusmethodnotallowed)
// io.writestring(w, "405 must connect\n")
// return
// }
// conn, _, err := w.(http.hijacker).hijack()
// if err != nil {
// log.print("rpc hijacking ", req.remoteaddr, ": ", err.error())
// return
// }
// io.writestring(conn, "http/1.0 "+connected+"\n\n")
// server.serveconn(conn)
//}
func errorrep(w http.ResponseWriter, errorcdoe int) {
w.WriteHeader(errorcdoe)
w.Write([]byte("error"))
}
func errormsgrep(w http.ResponseWriter, errorcode int32) {
v := &JsonGenerateUidResult{}
v.Result = errorcode
sendJson(w, v)
}
func successrep(w http.ResponseWriter, res int32, uid uint32, name string) {
v := &JsonGenerateUidResult{}
v.Result = res
v.Uid = uid
v.Name = name
sendJson(w, v)
}
func GenerateMd5Value(name string, sid uint32, st int64, salt string) string {
plaintext := fmt.Sprintf("[%v#%v#%v#%v]", name, sid, st, salt)
m5 := md5.New()
m5.Write([]byte(plaintext))
t := hex.EncodeToString(m5.Sum(nil))
return t
//by := m5.Sum([]byte(plaintext))
//if len(by) == 64 {
// return fmt.Sprintf("%x", by[16:48]) //32byte
//}
//t := fmt.Sprintf("%x", by)
//return t //string(m5.Sum([]byte(plaintext)))
//32 get 16 [8:24]
//64 get 32 [16:48]
}
func doGenerateName(tableindex uint32, w http.ResponseWriter, r *http.Request) {
params := geturlparams(r)
remoteip := RetmoteIp(r)
if len(params) > 0 {
vname, ok1 := params["uname"]
//vhandshakekey, ok2 := params["md5"]
vmd5, ok2 := params["md5"]
vserverid, ok3 := params["sid"]
vtime, ok4 := params["st"]
if ok1 && ok2 && ok3 && ok4 {
log.Printf("uname=%v md5=%v sid=%v st=%v", vname, vmd5, vserverid, vtime)
sid, err1 := strconv.Atoi(vserverid)
st, err2 := strconv.ParseInt(vtime, 10, 64)
if err1 != nil || err2 != nil {
log.Printf("ip=%v vserver=%v name=%v", remoteip, vserverid, vname)
errormsgrep(w, ERROR_NAME_INVALIDL)
return
}
if vmd5 != GenerateMd5Value(vname, uint32(sid), st, getMd5Salt()) {
if !isDebugMode() {
//回复错误
errormsgrep(w, ERROR_HANDSHAKE_KEY)
return
}
}
curtime := time.Now().Unix()
if curtime > st+60 {
if !isDebugMode() {
//回复错误
errormsgrep(w, ERROR_TIME_INVALIDL)
return
}
}
if len(vname) > 3 && len(vname) <= 64 && checkstring(vname) {
//合法
if uid, _ := GenerateName(tableindex, vname, false); uid > 0 {
successrep(w, ERROR_NONE, uid, vname)
} else {
errormsgrep(w, ERROR_NAME_DUPLICATE)
}
} else if len(vname) <= 1 { //空
//合法
if uid, tanme := GenerateUidOnly(tableindex); uid > 0 {
successrep(w, SUCESS_GENERATE_NAME, uid, tanme)
} else {
errormsgrep(w, ERROR_NAME_DUPLICATE)
}
} else {
log.Printf("ip=%v vserver=%v name=%v", remoteip, vserverid, vname)
errormsgrep(w, ERROR_NAME_INVALIDL)
}
return
}
}
errorrep(w, 404)
}
func handlerGenerateUserUid(w http.ResponseWriter, r *http.Request) {
doGenerateName(gdef.TYPE_TABLE_USER, w, r)
}
func handlerGenerateFamilyUid(w http.ResponseWriter, r *http.Request) {
doGenerateName(gdef.TYPE_TABLE_FAMILY, w, r)
}
//func clientfun(w http.ResponseWriter, r *http.Request) {
//
// r.ParseForm()
//
// path := r.URL.Path
// remoteip := RetmoteIp(r)
// log.Printf("path =%v remoteip=%v \n", path, remoteip)
// //strings.Split(r.RemoteAddr,":")
// switch path {
// case "/get_serverinfo": //得到服务器版本信息
// v := &jsondata.Serverinfo{}
// v.Version = "2017081011"
// v.State = int32(time.Now().Unix()) //正常
// sendJson(w, v)
// case "/login":
// param_account, f1 := r.Form["account"]
// param_token, f2 := r.Form["token"]
// if f1 && f2 && len(param_account[0]) > 1 {
// doLogin(w, param_account[0], param_token[0], remoteip)
// }
// }
//}
func SetUpHttpServer(httpaddr string, maxnum int32, md5salt string, bdebug bool) net.Listener {
httpser.maxnum = maxnum
httpser.debugmode = bdebug //true debug false release default false
httpser.md5salt = md5salt //握手key
log.Printf("httpaddr:%v maxnum=%v \n", httpaddr, maxnum)
l, err := net.Listen("tcp", httpaddr)
if err != nil {
log.Fatal("listen:", err)
}
lis := netutil.LimitListener(l, int(maxnum))
http.HandleFunc("/uuid", handlerGenerateUserUid)
http.HandleFunc("/fuid", handlerGenerateFamilyUid)
//先清理下
ClearCurIndexRedisDb()
if !QueryMaxUserId() {
log.Fatal("QueryMaxUserId:")
}
QueryAllUserName()
if !QueryMaxFamilyId() {
log.Fatal("QueryMaxFamilyId:")
}
QueryAllFamilyName()
/
go http.Serve(lis, nil)
//go http.Serve(lis, http.HandlerFunc(clientfun))
return lis
}
func QueryMaxUserId() bool {
querytimeout := time.Second * 10 //30秒
sort := bson.D{{"uid", -1}}
uid, err := mongodb.GetMongoSyncInstance().FindOneMax(sort, gdef.TABLE_USER_UID, querytimeout)
if err != nil {
log.Printf("QueryMaxUserId error=%v", err.Error())
return false
}
log.Printf("QueryMaxUserId(%v)", uid)
if uid < gdef.BASE_USER_ID {
uid = gdef.BASE_USER_ID
}
GetHttpSerInstance().SetUserId(uid)
return true
}
func QueryMaxFamilyId() bool {
querytimeout := time.Second * 10 //30秒
sort := bson.D{{"uid", -1}}
uid, err := mongodb.GetMongoSyncInstance().FindOneMax(sort, gdef.TABLE_Family_UID, querytimeout)
if err != nil {
log.Printf("QueryMaxFamilyId error=%v", err.Error())
return false
}
log.Printf("QueryMaxFamilyId(%v)", uid)
if uid < gdef.BASE_FAMILY_ID {
uid = gdef.BASE_FAMILY_ID
}
GetHttpSerInstance().SetFamilyId(uid)
return true
}
func QueryAllUserName() int64 {
//如果想指定 <projection> 是包含字段,那所有字段值都得统一是 1,相反如果是不包含,也必须都是 0
//projection 里要不写1 要么写0 不能混合在里面
projection := bson.D{
{"uid", 0},
// {"name", 1},
{"_id", 0},
}
//
mongodb_table := gdef.TABLE_USER_UID
redis_table := gdef.REDIS_TABLE_USER
querytimeout := time.Second * 30 //30秒
cursor, err := mongodb.GetMongoSyncInstance().FindAll(projection, mongodb_table, querytimeout)
if err != nil {
return -1
}
user := new(gdef.UserInfo)
rs := redis2.GetRedisSyncClient()
for cursor.Next(context.Background()) {
if err = cursor.Decode(user); err != nil {
return -2
}
rs.SAdd_ReturnVoid(redis_table, user.Name)
}
totalnum := rs.SCard(redis_table)
log.Printf("QueryName %v(%v)", mongodb_table, totalnum)
return totalnum
}
func QueryAllFamilyName() int64 {
//如果想指定 <projection> 是包含字段,那所有字段值都得统一是 1,相反如果是不包含,也必须都是 0
//projection 里要不写1 要么写0 不能混合在里面
projection := bson.D{
{"uid", 0},
// {"name", 1},
{"_id", 0},
}
//
mongodb_table := gdef.TABLE_Family_UID
redis_table := gdef.REDIS_TABLE_FAMILY
querytimeout := time.Second * 30 //30秒
cursor, err := mongodb.GetMongoSyncInstance().FindAll(projection, mongodb_table, querytimeout)
if err != nil {
return -1
}
user := new(gdef.FamilyInfo)
rs := redis2.GetRedisSyncClient()
for cursor.Next(context.Background()) {
if err = cursor.Decode(user); err != nil {
return -2
}
rs.SAdd_ReturnVoid(redis_table, user.Name)
}
totalnum := rs.SCard(redis_table)
log.Printf("QueryName %v(%v)", mongodb_table, totalnum)
return totalnum
}
func CheckNameVaild(name string) bool {
namelen := len(name)
if namelen < 3 {
return false
}
//user_ family_
if (namelen > 5 && name[:5] == "user_") || (namelen > 7 && name[:7] == "family_") {
return false
}
return true
}
func GenerateName(tableindex uint32, name string, bGenerateName bool) (uint32, string) {
mongodb_table := ""
redis_table := ""
// fGetUid := func() uint32 { return 0 }
switch tableindex {
case gdef.TYPE_TABLE_USER:
mongodb_table = gdef.TABLE_USER_UID
redis_table = gdef.REDIS_TABLE_USER
// fGetUid = GetHttpSerInstance().GetValidUserId
// data = &gdef.UserInfo{}
case gdef.TYPE_TABLE_FAMILY:
mongodb_table = gdef.TABLE_Family_UID
redis_table = gdef.REDIS_TABLE_FAMILY
// fGetUid = GetHttpSerInstance().GetValidFamilyId
// data = &gdef.FamilyInfo{}
default:
return 0, ""
}
rs := redis2.GetRedisSyncClient()
mgdb := mongodb.GetMongoSyncInstance()
if CheckNameVaild(name) {
nRet := rs.SAdd(redis_table, name)
switch {
case nRet < 0:
return 0, "Error"
case nRet == 0: //重名了
return 0, "DuplicateName"
default: //1 ok
break
}
} else {
return 0, "Name len error"
}
inserttimeout := time.Second * 3 //3秒
plaintext_name := name //明文name
switch tableindex {
case gdef.TYPE_TABLE_USER:
uid := GetHttpSerInstance().GetValidUserId()
//if bGenerateName && len(name) < 3 {
// plaintext_name = " user_" + strconv.Itoa(int(uid))
// name = base64.URLEncoding.EncodeToString([]byte(plaintext_name))
//}
data := &gdef.UserInfo{uid, plaintext_name}
if _, err := mgdb.InsertData(data, mongodb_table, inserttimeout); err == nil {
// rs.SAdd(redis_table, name)
return uid, name
}
rs.SRem(redis_table, name)
case gdef.TYPE_TABLE_FAMILY:
uid := GetHttpSerInstance().GetValidFamilyId()
//if bGenerateName && len(name) < 3 {
// plaintext_name = "family_" + strconv.Itoa(int(uid))
// name = base64.URLEncoding.EncodeToString([]byte(plaintext_name))
//}
data := &gdef.FamilyInfo{uid, plaintext_name}
if _, err := mgdb.InsertData(data, mongodb_table, inserttimeout); err == nil {
//rs.SAdd(redis_table, name)
return uid, name
} else {
rs.SRem(redis_table, name)
}
}
return 0, "Errorother"
}
func GenerateUidOnly(tableindex uint32) (uint32, string) {
mongodb_table := ""
// redis_table := ""
name := ""
// fGetUid := func() uint32 { return 0 }
switch tableindex {
case gdef.TYPE_TABLE_USER:
mongodb_table = gdef.TABLE_USER_UID
// redis_table = gdef.REDIS_TABLE_USER
// fGetUid = GetHttpSerInstance().GetValidUserId
// data = &gdef.UserInfo{}
case gdef.TYPE_TABLE_FAMILY:
mongodb_table = gdef.TABLE_Family_UID
// redis_table = gdef.REDIS_TABLE_FAMILY
// fGetUid = GetHttpSerInstance().GetValidFamilyId
// data = &gdef.FamilyInfo{}
default:
return 0, ""
}
// rs := redis2.GetRedisSyncClient()
mgdb := mongodb.GetMongoSyncInstance()
inserttimeout := time.Second * 3 //3秒
plaintext_name := "" //明文name
switch tableindex {
case gdef.TYPE_TABLE_USER:
uid := GetHttpSerInstance().GetValidUserId()
plaintext_name = " user_" + strconv.Itoa(int(uid))
name = base64.URLEncoding.EncodeToString([]byte(plaintext_name))
log.Printf("temp return uid(%v) name(%v)", uid, name)
//return uid, name
//下面的插入有问题,有时间再修正下
data := &gdef.UserInfo{uid, plaintext_name}
if _, err := mgdb.InsertData(data, mongodb_table, inserttimeout); err == nil {
//rs.SAdd(redis_table, name)
return uid, name
} else {
log.Printf("mgdb.InsertData error=%v", err.Error())
}
case gdef.TYPE_TABLE_FAMILY:
uid := GetHttpSerInstance().GetValidFamilyId()
plaintext_name = "family_" + strconv.Itoa(int(uid))
name = base64.URLEncoding.EncodeToString([]byte(plaintext_name))
data := &gdef.FamilyInfo{uid, plaintext_name}
if _, err := mgdb.InsertData(data, mongodb_table, inserttimeout); err == nil {
return uid, name
}
}
return 0, "Errorother"
}
// 清理当前索引的redis DB
func ClearCurIndexRedisDb() {
redis2.GetRedisSyncClient().FlushDB()
}
/*
//for _, v := range username {
// rediscli.SAdd(tableuser, v)
//}
//realnum := rediscli.SCard(tableuser).Val()
//if int64(namecount) != realnum {
// log.Fatal("queryallrolename num is equal org=%v read=%v ", namecount, realnum)
//}
//doc := cursor.Current
//doc.Index(0)
//assert.False(t, containsKey(doc, "_id"))
//assert.True(t, containsKey(doc, "item"))
//assert.True(t, containsKey(doc, "status"))
//assert.False(t, containsKey(doc, "size"))
//assert.False(t, containsKey(doc, "instock"))
//for cursor.Next(context.TODO()) {
// // A new result variable should be declared for each document.
// var result bson.M
// if err := cursor.Decode(&result); err != nil {
// log.Fatal(err)
// }
// fmt.Println(result)
//}
*/
4:测试,这个没什么好测试的,
有空可以找找原来用openresty+mysql做的,
这里的demo工程有需要再上传
如果觉得有用,麻烦点个赞,加个收藏