本篇概要:
11. 新增数据、封装 DB 初步、结合 Gin 实现查询 API;
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `topics`;
CREATE TABLE `topics` (
`topic_id` int(11) NOT NULL AUTO_INCREMENT,
`topic_title` varchar(200) NOT NULL,
`topic_short_title` varchar(50) DEFAULT NULL,
`user_ip` varchar(20) NOT NULL,
`topic_score` int(11) DEFAULT NULL,
`topic_url` varchar(200) NOT NULL,
`topic_date` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
PRIMARY KEY (`topic_id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8mb4;
INSERT INTO `topics` VALUES ('8', 'TopicTitle', 'TopicShortTitle', '127.0.0.1', '0', 'testurl', '2019-03-07 22:01:25');
新增数据
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicModel.go
package src
import "time"
type Topics struct { //单个Topic实体
TopicID int `json:"id" gorm:"PRIMARY_KEY"`
TopicTitle string `json:"title" binding:"min=4,max=20"`
TopicShortTitle string `json:"stitle" binding:"required,nefield=TopicTitle"`
UserIp string `json:"ip" binding:"ipv4"`
TopicScore int `json:"score" binding:"omitempty,gt=5"`
TopicUrl string `json:"url" binding:"omitempty,topicurl"`
TopicDate time.Time `json:"url" binding:"required"`
}
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"time"
. "topic.gin.test.com/src"
)
func main() {
dsn := "root:asdf@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
// 新增数据
topics := Topics{
TopicTitle:"大标题",
TopicShortTitle:"小标题",
UserIp: "127.0.0.1",
TopicScore: 2,
TopicUrl: "xxx.com/?article=12",
TopicDate: time.Now(),
}
fmt.Println(db.Create(&topics).RowsAffected) // RowsAffected 判断是否插入成功
fmt.Println(topics.TopicID)
}
封装 DB:测试 http://localhost:8080/v1/topics/9
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/MyDB.go
package src
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
var DBHelper *gorm.DB
var err error
func init() {
dsn := "root:asdf@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
// 此处等于
DBHelper, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println(err)
}
//DBHelper.Debug()
}
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicModel.go
package src
import "time"
type Topics struct { //单个Topic实体
TopicID int `json:"id" gorm:"PRIMARY_KEY"`
TopicTitle string `json:"title" binding:"min=4,max=20"`
TopicShortTitle string `json:"stitle" binding:"required,nefield=TopicTitle"`
UserIp string `json:"ip" binding:"ipv4"`
TopicScore int `json:"score" binding:"omitempty,gt=5"`
TopicUrl string `json:"url" binding:"omitempty,topicurl"`
TopicDate time.Time `json:"url" binding:"required"`
}
type TopicArray struct {
TopicList []Topics `json:"topics" binding:"gt=0,lt=3,topics,dive"`
TopicListSize int `json:"size"`
}
type TopicClass struct {
ClassId int `gorm:"primaryKey"` // 主键
ClassName string
ClassRemark string
ClassType string `gorm:"Column:classtype"` //指定字段名
}
// 单个 Topic 实体
type Topic struct {
//TopicID int `json:"id"` // 映射
//TopicTitle string `json:"title" binding:"required"` // require 必须传参
TopicID int `json:"id"`
TopicTitle string `json:"title" binding:"min=4,max=20"` // 最小4,最大20字符
TopicShortTitle string `json:"stitle" binding:"required,nefield=TopicTitle"` // 短标题,nefield(not equal),和xx字段不能相等
UserIP string `json:"ip" binding:"ipv4"`
TopicScore int `json:"score" binding:"omitempty,gt=5"` // 帖子积分,omitempty 可空,填了必须大于5
}
// 多条
//type Topics struct {
// TopicList []Topic `json:"topics" binding:"gt=0,lt=3,dive"` // 切片,dive进一步验证单条实体
// TopicListSize int `json:"size"`
//}
// 创建实体
func CreateTopic(id int, title string) Topic {
// return Topic{id, title} // 只有两个字段,多字段建议写字段
return Topic{TopicID:id, TopicTitle:title}
}
// 参数绑定
type TopicQuery struct {
UserName string `json:"username" form:"username"`
Page int `json:"page" form:"page" binding:"required"` // binding 必须有
PageSize int `json:"pagesize" form:"pagesize"`
}
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicDao.go
package src
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 登录判断,必须登录
func MustLogin() (gin.HandlerFunc) {
return func(c *gin.Context) {
if _, status := c.GetQuery("token"); !status {
c.String(http.StatusUnauthorized, "缺少token参数")
c.Abort()
} else {
c.Next() // 不写也会往下走
}
}
}
// 有返回,调用 GetTopic 加 "()"
func GetTopic() (gin.HandlerFunc) {
return func(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
}
}
// !处理数据库相关操作
func GetTopicDetail(c *gin.Context) {
// c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
// c.JSON(200, CreateTopic(101, "帖子标题"))
// 单独查询
//dsn := "root:asdf@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
//db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
//tid := c.Param("topic_id")
//topics := Topics{}
//db.Find(&topics, tid)
//c.JSON(200, topics)
tid := c.Param("topic_id")
topics := Topics{}
DBHelper.Find(&topics, tid)
c.JSON(200, topics)
}
// 单帖新增
func NewTopic(c *gin.Context) {
//判断登录
topic:=Topic{}
err:=c.BindJSON(&topic) // 寻找 json 参数
if err!=nil{
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,topic)
}
}
// 多帖批量新增
func NewTopics(c *gin.Context) {
//判断登录
topics:=Topics{}
err:=c.BindJSON(&topics) // 寻找 json 参数
if err!=nil{
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,topics)
}
}
func DelTopic(c *gin.Context) {
//判断登录
c.String(200,"删除帖子")
}
func GetTopicList(c *gin.Context) {
query := TopicQuery{}
err := c.BindQuery(&query)
if err != nil{
// 自定义错误信息
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,query)
}
}
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"github.com/gin-gonic/gin"
. "topic.gin.test.com/src"
)
func main() {
router:=gin.Default()
v1:=router.Group("/v1/topics") // 单条帖子
{ // 代码块
v1.GET("", GetTopicList)
// 注意这里是不能加小括号的。否则变成执行 GetTopicDetail 函数了
v1.GET("/:topic_id", GetTopicDetail)
v1.Use(MustLogin())
{
v1.POST("",NewTopic)
v1.DELETE("/:topic_id", DelTopic)
}
}
// 多条帖子
v2:=router.Group("/v1/mtopics")
{
v2.Use(MustLogin())
{
v2.POST("",NewTopics)
}
}
router.Run()
}
12. 简单连接池设置、信号处理、优雅的退出程序;
查看数据库连接池
show PROCESSLIST
连接池基本参数
// 文档:https://gorm.io/zh_CN/docs/generic_interface.html
// db.DB() 可以返回 sql.db 对象
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/MyDB.go
package src
import (
"fmt"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"time"
)
var DBHelper *gorm.DB
var err error
func init() {
dsn := "root:asdf@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
DBHelper, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
fmt.Println(err)
}
sqlDB, _ := DBHelper.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
}
代码演示,强行停止会有 Process finished with exit code 2
func main() {
count:=0
for {
fmt.Println("执行",count)
count++
time.Sleep(time.Second*1)
}
}
关于信号
- 当我们按ctrl+c时,会发出SIGINT (这值是2),默认就是进程终止
- 其他类似
- SIGTERM :kill命令的默认信号 (信号值是15, 也就是通常的kill -15进程id)
- SIGILL :kill -9
- SIGQUIT :建立CORE文件终止进程,并且生成core文件
func main () {
count:=0
go func() {
for {
fmt.Println("执行",count)
count++
time.Sleep(time.Second*1)
}
}()
c:=make(chan os.Signal) // 创建信号chan
go func() {
// 创建一个 超时context,到期后会执行Done
ctx,_:=context.WithTimeout(context.Background(),time.Second*5)
select {
case <-ctx.Done():
// 重点,超时时间到了会发送SIGINT信号
c<-os.Interrupt
}
}()
// 监听信号
signal.Notify(c)
s:=<-c
fmt.Println(s)
}
13. 数据库连接出错时关闭 web 服务:两种方式;
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/MyInit.go
package src
import (
"log"
"os"
"os/signal"
)
var ServerSigChan chan os.Signal
func init() {
ServerSigChan = make(chan os.Signal)
}
func ShutDownServer(err error) {
log.Println(err)
ServerSigChan<-os.Interrupt
}
func ServerNotify() {
signal.Notify(ServerSigChan,os.Interrupt)
<-ServerSigChan
}
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/MyDB.go
package src
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"time"
)
var DBHelper *gorm.DB
var err error
func InitDB() {
dsn := "root:asdf@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
// 此处等于
DBHelper, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
ShutDownServer(err)
return
//log.Fatal("DB初始化错误:", err)
//fmt.Println(err)
}
sqlDB, _ := DBHelper.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
}
package main
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"os/signal"
"time"
. "topic.gin.test.com/src"
)
func main() {
router:=gin.Default()
v1:=router.Group("/v1/topics") // 单条帖子
{ // 代码块
v1.GET("", GetTopicList)
// 注意这里是不能加小括号的。否则变成执行 GetTopicDetail 函数了
v1.GET("/:topic_id", GetTopicDetail)
v1.Use(MustLogin())
{
v1.POST("",NewTopic)
v1.DELETE("/:topic_id", DelTopic)
}
}
// 多条帖子
v2:=router.Group("/v1/mtopics")
{
v2.Use(MustLogin())
{
v2.POST("",NewTopics)
}
}
// router.Run()
server := &http.Server{
Addr:":8080",
Handler:router,
}
go(func() { // 启动web服务
err := server.ListenAndServe()
if err!=nil{
log.Fatal("服务器启动失败")
}
})()
go(func() {
InitDB()
})()
ServerNotify()
//这里还可以做一些 释放连接或善后工作,暂时略
ctx,cancel:=context.WithTimeout(context.Background(),time.Second*5)
defer cancel()
err:=server.Shutdown(ctx)
if err!=nil{
log.Fatalln("服务器关闭")
}
log.Println("服务器优雅退出")
}
14. redis 第三方库、连接池;
地址:https://github.com/gomodule/redigo
文档:https://godoc.org/github.com/gomodule/redigo/redis#pkg-examples
安装:go get github.com/gomodule/redigo/redis
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/MyRedis.go
package src
import (
"github.com/gomodule/redigo/redis"
"time"
)
// 参考:https://pkg.go.dev/github.com/gomodule/redigo/redis#NewPool
var RedisDefaultPool *redis.Pool
func newPool(addr string) *redis.Pool {
return &redis.Pool{
MaxIdle: 3,
IdleTimeout: 240 * time.Second,
// Dial or DialContext must be set. When both are set, DialContext takes precedence over Dial.
Dial: func () (redis.Conn, error) { return redis.Dial("tcp", addr) },
}
}
func init() {
RedisDefaultPool=newPool("127.0.0.1:6379")
}
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"context"
"fmt"
"github.com/gomodule/redigo/redis"
"log"
"os"
"os/signal"
"time"
. "topic.gin.test.com/src"
)
func main() {
// redis
conn := RedisDefaultPool.Get()
ret, err := redis.String(conn.Do("get","name")) // 可执行 redis 原生命令
if err != nil {
log.Println(err)
return
}
log.Println(ret)
}
15. 结合 gin 实现基本的 redis 缓存、缓存穿透简单处理;
之前我们实现一个 AP I是 GET /topic/8
,现在最基本最简单的缓存是
-
根据ID 查看数据库是否有值,如果有则取 redis的内容并返回、如果没有,则从数据库取 。
-
取出来后 放入redis缓存,并设置过期时间
-
文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicDao.go
package src
import (
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
"github.com/pquerna/ffjson/ffjson"
"log"
"net/http"
)
// 登录判断,必须登录
func MustLogin() (gin.HandlerFunc) {
return func(c *gin.Context) {
if _, status := c.GetQuery("token"); !status {
c.String(http.StatusUnauthorized, "缺少token参数")
c.Abort()
} else {
c.Next() // 不写也会往下走
}
}
}
// 有返回,调用 GetTopic 加 "()"
func GetTopic() (gin.HandlerFunc) {
return func(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
}
}
// !处理数据库相关操作
func GetTopicDetail(c *gin.Context) {
// c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
// c.JSON(200, CreateTopic(101, "帖子标题"))
// 单独查询
//dsn := "root:asdf@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
//db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
//tid := c.Param("topic_id")
//topics := Topics{}
//db.Find(&topics, tid)
//c.JSON(200, topics)
//tid := c.Param("topic_id")
//topics := Topics{}
//DBHelper.Find(&topics, tid)
//c.JSON(200, topics)
// 缓存!
tid := c.Param("topic_id")
topics := Topics{}
conn := RedisDefaultPool.Get()
defer conn.Close()
redisKey := "topic_"+tid
ret, err := redis.Bytes(conn.Do("get", redisKey))
if err != nil { // 缓存无值
DBHelper.Find(&topics, tid)
retData, _ := ffjson.Marshal(topics)
if topics.TopicID == 0 { //代表从数据库没有匹配到
conn.Do("setex",redisKey,20,retData) // 防止缓存穿透,时间短一点
} else { //正常数据 50秒缓存
conn.Do("setex",redisKey,50,retData)
}
c.JSON(200,topics)
log.Println("从数据库读取")
} else { // 缓存有值
log.Println("从 redis 读取")
ffjson.Unmarshal(ret, &topics)
c.JSON(200,topics)
}
}
// 单帖新增
func NewTopic(c *gin.Context) {
//判断登录
topic:=Topic{}
err:=c.BindJSON(&topic) // 寻找 json 参数
if err!=nil{
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,topic)
}
}
// 多帖批量新增
func NewTopics(c *gin.Context) {
//判断登录
topics:=Topics{}
err:=c.BindJSON(&topics) // 寻找 json 参数
if err!=nil{
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,topics)
}
}
func DelTopic(c *gin.Context) {
//判断登录
c.String(200,"删除帖子")
}
func GetTopicList(c *gin.Context) {
query := TopicQuery{}
err := c.BindQuery(&query)
if err != nil{
// 自定义错误信息
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,query)
}
}
16. 使用 “装饰器模式” 实现 Redis 缓存的封装基本套路;
装饰器函数
// 其实就是一个高阶函数
func CacheDecorator(h gin.HandlerFunc) gin.HandlerFunc{
return func(context *gin.Context) {
}
}
// 追加参数使其更通通
func CacheDecorator(h gin.HandlerFunc,param string,redKeyPattern string,empty interface{})
// param是获取的参数ID,因为装饰器并不知道获取的ID参数是什么
// redKeyPattern 是redis中key的格式 ,因为装饰器也并不知道redis存的key是什么形式
// empty 传入一个空对象,用于转化
// 路由部分
v1.GET("/:topic_id",CacheDecorator(GetTopicDetail))
// 目标函数修改
func GetTopicDetail(c *gin.Context) {
tid := c.Param("topic_id")
topics := Topics{}
DBHelper.Find(&topics,tid)
c.Set("dbResult",topics) // 关键部分
}
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/Decorator.go
package src
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/gomodule/redigo/redis"
"github.com/pquerna/ffjson/ffjson"
"log"
)
// 缓存装饰器
func CacheDecorator(h gin.HandlerFunc, param string, redKeyPattern string, empty interface{}) gin.HandlerFunc {
return func(context *gin.Context) {
//redis判断
getID := context.Param(param) //得到ID值
redisKey := fmt.Sprintf(redKeyPattern, getID)
conn := RedisDefaultPool.Get()
defer conn.Close()
ret, err := redis.Bytes(conn.Do("get", redisKey))
if err!=nil { // 缓存里没有
h(context) // 执行目标方法
dbResult, exists := context.Get("dbResult")
if !exists{
dbResult = empty
}
retData, _ := ffjson.Marshal(dbResult)
conn.Do("setex",redisKey,20,retData)
context.JSON(200,dbResult)
log.Println("从数据库读取")
} else {//缓存有 ,直接抛出
log.Println("从 redis读取")
ffjson.Unmarshal(ret,&empty)
context.JSON(200,empty)
}
}
}
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/src/TopicDao.go
package src
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 登录判断,必须登录
func MustLogin() (gin.HandlerFunc) {
return func(c *gin.Context) {
if _, status := c.GetQuery("token"); !status {
c.String(http.StatusUnauthorized, "缺少token参数")
c.Abort()
} else {
c.Next() // 不写也会往下走
}
}
}
// 有返回,调用 GetTopic 加 "()"
func GetTopic() (gin.HandlerFunc) {
return func(c *gin.Context) {
c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
}
}
// !!处理数据库相关操作
func GetTopicDetail(c *gin.Context) {
// c.String(200,"获取topicid=%s的帖子",c.Param("topic_id"))
// c.JSON(200, CreateTopic(101, "帖子标题"))
// 单独查询
//dsn := "root:asdf@tcp(127.0.0.1:3306)/test?charset=utf8mb4&parseTime=True&loc=Local"
//db, _ := gorm.Open(mysql.Open(dsn), &gorm.Config{})
//tid := c.Param("topic_id")
//topics := Topics{}
//db.Find(&topics, tid)
//c.JSON(200, topics)
//tid := c.Param("topic_id")
//topics := Topics{}
//DBHelper.Find(&topics, tid)
//c.JSON(200, topics)
// redis
//tid := c.Param("topic_id")
//topics := Topics{}
//
//conn := RedisDefaultPool.Get()
//defer conn.Close()
//
//redisKey := "topic_"+tid
//
//ret, err := redis.Bytes(conn.Do("get", redisKey))
//if err != nil { // 缓存无值
// DBHelper.Find(&topics, tid)
// retData, _ := ffjson.Marshal(topics)
// if topics.TopicID == 0 { //代表从数据库没有匹配到
// conn.Do("setex",redisKey,20,retData) // 防止缓存穿透,时间短一点
// } else { //正常数据 50秒缓存
// conn.Do("setex",redisKey,50,retData)
// }
// c.JSON(200,topics)
// log.Println("从数据库读取")
//} else { // 缓存有值
// log.Println("从 redis 读取")
// ffjson.Unmarshal(ret, &topics)
// c.JSON(200,topics)
//}
tid:=c.Param("topic_id")
topics:=Topics{}
DBHelper.Find(&topics,tid)//从数据库取
c.Set("dbResult",topics)
}
// 单帖新增
func NewTopic(c *gin.Context) {
//判断登录
topic:=Topic{}
err:=c.BindJSON(&topic) // 寻找 json 参数
if err!=nil{
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,topic)
}
}
// 多帖批量新增
func NewTopics(c *gin.Context) {
//判断登录
topics:=Topics{}
err:=c.BindJSON(&topics) // 寻找 json 参数
if err!=nil{
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,topics)
}
}
func DelTopic(c *gin.Context) {
//判断登录
c.String(200,"删除帖子")
}
func GetTopicList(c *gin.Context) {
query := TopicQuery{}
err := c.BindQuery(&query)
if err != nil{
// 自定义错误信息
c.String(400,"参数错误:%s",err.Error())
}else{
c.JSON(200,query)
}
}
- 文件
/Users/hualaoshuan/go/go-gin/api.gin.test.com/topic/main.go
package main
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"log"
"net/http"
"os"
"os/signal"
"time"
. "topic.gin.test.com/src"
)
func main() {
router:=gin.Default()
v1:=router.Group("/v1/topics") // 单条帖子
{ // 代码块
v1.GET("", GetTopicList)
// 注意这里是不能加小括号的。否则变成执行 GetTopicDetail 函数了
// v1.GET("/:topic_id", GetTopicDetail)
// 调用装饰器
v1.GET("/:topic_id",CacheDecorator(GetTopicDetail, "topic_id", "topic_%s", Topics{}))
v1.Use(MustLogin())
{
v1.POST("",NewTopic)
v1.DELETE("/:topic_id", DelTopic)
}
}
// 多条帖子
v2:=router.Group("/v1/mtopics")
{
v2.Use(MustLogin())
{
v2.POST("",NewTopics)
}
}
// router.Run()
server := &http.Server{
Addr:":8080",
Handler:router,
}
go(func() { // 启动web服务
err := server.ListenAndServe()
if err!=nil{
log.Fatal("服务器启动失败")
}
})()
go(func() {
InitDB()
})()
ServerNotify()
//这里还可以做一些 释放连接或善后工作,暂时略
ctx,cancel:=context.WithTimeout(context.Background(),time.Second*5)
defer cancel()
err:=server.Shutdown(ctx)
if err!=nil{
log.Fatalln("服务器关闭")
}
log.Println("服务器优雅退出")
}