项目地址:github
学习go已有一段时间,自觉可以做些什么。受社区的启发,便把构建一个属于自己的简书网站当作课业,并决定同时写下此篇,用于给自己厘清思路,发散思维,不至于全在脑子里混沌,一团浆糊,终不成事。
课业规划,均来自社区课程:《用Go实现一个简书》。
建立Go项目
go mod init github.com/unrotten/hello-world-web
新建项目后,在项目目录中,使用go modules初始化项目。
建立目录如下:
- cmd/hello-world-web:存放程序入口main.go文件
- config:存放配置文件
- controller:由于本项目将使用GraphQL API——graphql库实现,故而此目录将用于graphQL的定义
- middlewire:中间件
- model:结构体定义
- resolve:关于graphQL的具体实现
- setting:加载配置项
- static:前端目录
- util:工具包
初始化项目数据库
本项目使用Postgres数据库。
1、文章表
CREATE TYPE article_state as ENUM ('unaudited','online','offline','deleted')
CREATE TABLE public.article (
id int8 NOT NULL, -- 主键
sn varchar(32) NOT NULL, -- 文章序号
title varchar(255) NOT NULL, -- 文章标题
uid int8 NOT NULL, -- 作者id
cover varchar(255) NULL, -- 封面
"content" text NOT NULL, -- 内容,markdown格式
tags _varchar NULL, -- 文章标签
state article_state NOT NULL DEFAULT 'unaudited’::article_state, -- 状态:'unaudited'-未审核,'online'-已上线,'offline'-已下线,'deleted'-已删除
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 创建时间
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, -- 更新时间
deleted_at timestamp NOT NULL, -- 删除时间
CONSTRAINT article_pkey PRIMARY KEY (id),
CONSTRAINT sn UNIQUE (sn)
);
COMMENT ON COLUMN public.article.id IS '主键';
COMMENT ON COLUMN public.article.sn IS '文章序号';
COMMENT ON COLUMN public.article.title IS '文章标题';
COMMENT ON COLUMN public.article.uid IS '作者id';
COMMENT ON COLUMN public.article.cover IS '封面';
COMMENT ON COLUMN public.article."content" IS '内容,markdown格式';
COMMENT ON COLUMN public.article.tags IS '文章标签';
COMMENT ON COLUMN public.article.state IS '状态:''unaudited''-未审核,''online''-已上线,''offline''-已下线,''deleted''-已删除';
COMMENT ON COLUMN public.article.created_at IS '创建时间';
COMMENT ON COLUMN public.article.updated_at IS '更新时间';
COMMENT ON COLUMN public.article.deleted_at IS '删除时间';
2、文章扩展表
CREATE TABLE "public"."article_ex" (
"aid" int8 NOT NULL,
"view_num" int4 NOT NULL DEFAULT 0,
"cmt_num" int4 NOT NULL DEFAULT 0,
"zan_num" int4 NOT NULL DEFAULT 0,
"created_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" timestamp(6) NOT NULL,
PRIMARY KEY ("aid")
)
;
COMMENT ON COLUMN "public"."article_ex"."aid" IS '文章ID';
COMMENT ON COLUMN "public"."article_ex"."view_num" IS '浏览数';
COMMENT ON COLUMN "public"."article_ex"."cmt_num" IS '评论数';
COMMENT ON COLUMN "public"."article_ex"."zan_num" IS '点赞数';
COMMENT ON COLUMN "public"."article_ex"."created_at" IS '创建时间';
COMMENT ON COLUMN "public"."article_ex"."updated_at" IS '更新时间';
COMMENT ON COLUMN "public"."article_ex"."deleted_at" IS '删除时间';
COMMENT ON TABLE "public"."article_ex" IS '文章扩展表';
3、用户表
CREATE TYPE user_state as ENUM (‘unsign’,’normal,’forbidden’,’freeze’)
CREATE TYPE gender as ENUM (‘man’,’woman’,’unknown')
CREATE TABLE "public"."user" (
"id" int8 NOT NULL,
"username" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"email" varchar(32) COLLATE "pg_catalog"."default" NOT NULL,
"password" varchar(255) COLLATE "pg_catalog"."default" NOT NULL,
"avatar" varchar(127) COLLATE "pg_catalog"."default" NOT NULL,
"gender" "public"."gender" NOT NULL DEFAULT 'unknown'::gender,
"introduce" text COLLATE "pg_catalog"."default",
"state" "public"."user_state" NOT NULL DEFAULT 'unsign'::user_state,
"root" bool NOT NULL DEFAULT false,
"created_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" timestamp(6) NOT NULL,
PRIMARY KEY ("id")
)
;
COMMENT ON COLUMN "public"."user"."id" IS 'ID';
COMMENT ON COLUMN "public"."user"."username" IS '用户名';
COMMENT ON COLUMN "public"."user"."email" IS '注册邮箱';
COMMENT ON COLUMN "public"."user"."password" IS '密码';
COMMENT ON COLUMN "public"."user"."avatar" IS '头像';
COMMENT ON COLUMN "public"."user"."gender" IS '性别:''man''-男,''woman''-女,''unknown''-保密';
COMMENT ON COLUMN "public"."user"."introduce" IS '个人简介';
COMMENT ON COLUMN "public"."user"."state" IS '状态:''unsign''-未认证,''normal''-正常,''forbidden''-禁止发言,''freeze''-冻结';
COMMENT ON COLUMN "public"."user"."root" IS '是否管理员';
COMMENT ON COLUMN "public"."user"."created_at" IS '创建时间';
COMMENT ON COLUMN "public"."user"."updated_at" IS '更新时间';
COMMENT ON COLUMN "public"."user"."deleted_at" IS '删除时间';
4、用户计数表
CREATE TABLE "public"."user_count" (
"uid" int8 NOT NULL,
"fans_num" int4 NOT NULL DEFAULT 0,
"follow_num" int4 NOT NULL DEFAULT 0,
"article_num" int4 NOT NULL DEFAULT 0,
"words" int4 NOT NULL DEFAULT 0,
"zan_num" int4 NOT NULL DEFAULT 0,
"created_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" timestamp(6) NOT NULL,
PRIMARY KEY ("uid")
)
;
COMMENT ON COLUMN "public"."user_count"."uid" IS '用户ID';
COMMENT ON COLUMN "public"."user_count"."fans_num" IS '粉丝数';
COMMENT ON COLUMN "public"."user_count"."follow_num" IS '关注数(关注其他用户)';
COMMENT ON COLUMN "public"."user_count"."article_num" IS '文章数';
COMMENT ON COLUMN "public"."user_count"."words" IS '字数';
COMMENT ON COLUMN "public"."user_count"."zan_num" IS '被赞数';
COMMENT ON COLUMN "public"."user_count"."created_at" IS '创建时间';
COMMENT ON COLUMN "public"."user_count"."updated_at" IS '更新时间';
COMMENT ON COLUMN "public"."user_count"."deleted_at" IS '删除时间';
5、用户关注表
CREATE TABLE "public"."user_follow" (
"id" int8 NOT NULL,
"uid" int8 NOT NULL,
"fuid" int8 NOT NULL,
"created_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" timestamp(6) NOT NULL,
PRIMARY KEY ("id"),
CONSTRAINT "uq_uid_fuid" UNIQUE ("uid", "fuid")
)
;
COMMENT ON COLUMN "public"."user_follow"."id" IS 'ID';
COMMENT ON COLUMN "public"."user_follow"."uid" IS '用户ID';
COMMENT ON COLUMN "public"."user_follow"."fuid" IS '粉丝ID';
COMMENT ON COLUMN "public"."user_follow"."created_at" IS '创建时间';
COMMENT ON COLUMN "public"."user_follow"."updated_at" IS '更新时间';
COMMENT ON COLUMN "public"."user_follow"."deleted_at" IS '删除时间';
COMMENT ON TABLE "public"."user_follow" IS '用户关注表';
6、评论表
CREATE TABLE "public"."comment" (
"id" int8 NOT NULL,
"aid" int8 NOT NULL,
"uid" int8 NOT NULL,
"content" text NOT NULL,
"zan_num" int4 NOT NULL DEFAULT 0,
"floor" int4 NOT NULL DEFAULT 1,
"state" "public"."article_state" NOT NULL DEFAULT 'unaudited',
"created_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" timestamp(6) NOT NULL,
PRIMARY KEY ("id"),
CONSTRAINT "uq_aidfloor" UNIQUE ("aid", "floor")
)
;
COMMENT ON COLUMN "public"."comment"."id" IS 'id';
COMMENT ON COLUMN "public"."comment"."aid" IS '文章ID';
COMMENT ON COLUMN "public"."comment"."uid" IS '评论用户id';
COMMENT ON COLUMN "public"."comment"."content" IS '评论内容';
COMMENT ON COLUMN "public"."comment"."zan_num" IS '被赞数';
COMMENT ON COLUMN "public"."comment"."floor" IS '第几楼';
COMMENT ON COLUMN "public"."comment"."state" IS '状态:''unaudited''-未审核,''online''-已上线,''offline''-已下线,''deleted''-已删除';
COMMENT ON COLUMN "public"."comment"."created_at" IS '创建时间';
COMMENT ON COLUMN "public"."comment"."updated_at" IS '更新时间';
COMMENT ON COLUMN "public"."comment"."deleted_at" IS '删除时间';
COMMENT ON TABLE "public"."comment" IS '评论表';
7、评论回复表
CREATE TABLE "public"."comment_reply" (
"id" int8 NOT NULL,
"cid" int8 NOT NULL,
"uid" int8 NOT NULL,
"content" text NOT NULL,
"state" "public"."article_state" NOT NULL DEFAULT 'unaudited'::article_state,
"created_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" timestamp(6) NOT NULL,
PRIMARY KEY ("id")
)
;
CREATE INDEX "idx_cid" ON "public"."comment_reply" (
"cid"
);
COMMENT ON COLUMN "public"."comment_reply"."id" IS 'id';
COMMENT ON COLUMN "public"."comment_reply"."cid" IS '评论id';
COMMENT ON COLUMN "public"."comment_reply"."uid" IS '回复人id';
COMMENT ON COLUMN "public"."comment_reply"."content" IS '回复内容';
COMMENT ON COLUMN "public"."comment_reply"."state" IS '状态:''unaudited''-未审核,''online''-已上线,''offline''-已下线,''deleted''-已删除';
COMMENT ON COLUMN "public"."comment_reply"."created_at" IS '创建时间';
COMMENT ON COLUMN "public"."comment_reply"."updated_at" IS '更新时间';
COMMENT ON COLUMN "public"."comment_reply"."deleted_at" IS '删除时间';
COMMENT ON TABLE "public"."comment_reply" IS '评论回复表';
8、赞表
create type zan_type as ENUM ('article','comment','reply')
CREATE TABLE "public"."zan" (
"id" int8 NOT NULL,
"uid" int8 NOT NULL,
"objtype" "public"."zan_type" NOT NULL DEFAULT 'article',
"objid" int8 NOT NULL,
"created_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted_at" timestamp(6) NOT NULL,
PRIMARY KEY ("id"),
CONSTRAINT "uq_u_obj" UNIQUE ("uid", "objtype", "objid")
)
;
COMMENT ON COLUMN "public"."zan"."id" IS 'id';
COMMENT ON COLUMN "public"."zan"."uid" IS '点赞用户id';
COMMENT ON COLUMN "public"."zan"."objtype" IS '被点赞对象:';
COMMENT ON COLUMN "public"."zan"."objid" IS '被赞对象id';
COMMENT ON COLUMN "public"."zan"."created_at" IS '创建时间';
COMMENT ON COLUMN "public"."zan"."updated_at" IS '更新时间';
COMMENT ON COLUMN "public"."zan"."deleted_at" IS '删除时间';
COMMENT ON TABLE "public"."zan" IS '赞表';
编写Go项目配置
本项目使用viper库加载配置项。
在hello-world-web
项目目录下,执行 go get github.com/spf13/viper
。
拉取依赖包完成后,在config
目录下,新建配置文件config.toml
,内容如下:
#debug or release
run_mode = "debug"
[app]
jwt_secret = "20144481"
# 定义 HTTP 监听端口
[http]
port = "8008"
# 存储配置,使用Postgres
[storage]
user = "admin"
password = "admin"
host = "localhost"
port = 5432
dbname = "postgres"
# 日志设置
[logger]
file_path= "/Users/yan/GolandProjects/log/hello-world-web/"
# 使用zerolog包配置,0-debug,1-info,3-warn,4-error,5-fatal,6-panic,7-nolevel,8-disable, -1-> trace
level= 0
# mail配置
[mail]
host="smtp.gmail.com"
port=465
email="unrotten7@gmail.com"
password="******"
在setting包下,新建setting.go
文件:
package setting
import (
"fmt"
"github.com/spf13/viper"
)
var (
RunMode string
HttpPort string
MailHost string
MailPort int
MailAddr string
MailPwd string
JwtSecret string
)
func init() {
viper.AddConfigPath("config")
err := viper.ReadInConfig()
if err != nil {
panic(fmt.Errorf("读取配置文件失败: %s \n", err))
}
// 设置默认配置
viper.SetDefault("run_mode", "0")
viper.SetDefault("http.port", "8008")
viper.SetDefault("logger.level", "debug")
viper.SetDefault("storage.user", "admin")
viper.SetDefault("storage.password", "admin")
viper.SetDefault("storage.host", "localhost")
viper.SetDefault("storage.port", 5432)
viper.SetDefault("storage.dbname", "postgres")
// 获取配置信息
RunMode = viper.GetString("run_mode")
HttpPort = viper.GetString("http.port")
MailHost = viper.GetString("mail.host")
MailPort = viper.GetInt("mail.port")
MailAddr = viper.GetString("mail.email")
MailPwd = viper.GetString("mail.password")
JwtSecret = viper.GetString("app.jwt_secret")
}
首先使用viper.AddConfigPath("config")
添加配置文件路径,若需其他配置方法,可以参照如下配置方案:
viper.SetConfigName("config") // name of config file (without extension)
viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name
viper.AddConfigPath("/etc/appname/") // path to look for the config file in
viper.AddConfigPath("$HOME/.appname") // call multiple times to add many search paths
viper.AddConfigPath(".") // optionally look for config in the working directory
err := viper.ReadInConfig() // Find and read the config file
if err != nil { // Handle errors reading the config file
panic(fmt.Errorf("Fatal error config file: %s \n", err))
}
读取配置文件后,使用viper.SetDefault()
可对相应配置信息设置默认值。
当然viper还有更多用法,譬如动态加载配置信息:
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
fmt.Println("Config file changed:", e.Name)
})
更多用法可以参考官方文档。
在本项目中,日志库选择zerolog库,同样是社区课程推荐的日志库。这里只是粗略的将日志按配置好的格式记入指定的文件和标准输出中。同样使用者可以根据自身需求,将不同级别的日志分到不同的文件中,通过logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
获取指定的日志实例,分别记录不同级别的日志。工具包util
目录下编写logger.go
文件:
package util
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/rs/zerolog"
"github.com/spf13/viper"
"io"
"os"
"strings"
"sync"
"time"
)
var logOutPut zerolog.ConsoleWriter
var (
pool sync.Pool
)
func init() {
loggerFile := viper.GetString("logger.file_path")
loggerlevel := viper.GetInt("logger.level")
// 初始化日志配置
zerolog.SetGlobalLevel(zerolog.Level(loggerlevel))
if loggerFile == "" {
logOutPut = zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
} else {
file, err := os.Create(loggerFile)
if err != nil {
panic(fmt.Errorf("打开日志文件[%s]失败 \n", loggerFile))
}
gin.DefaultWriter = io.MultiWriter(file, os.Stdout)
logOutPut = zerolog.ConsoleWriter{Out: io.MultiWriter(file, os.Stdout), TimeFormat: time.RFC3339}
}
logOutPut.FormatLevel = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
}
logOutPut.FormatMessage = func(i interface{}) string {
if i != nil {
return fmt.Sprintf("***%s****", i)
}
return ""
}
logOutPut.FormatFieldName = func(i interface{}) string {
return fmt.Sprintf("%s:", i)
}
logOutPut.FormatFieldValue = func(i interface{}) string {
return strings.ToUpper(fmt.Sprintf("%s", i))
}
pool = sync.Pool{New: func() interface{} {
return zerolog.New(logOutPut).With().Timestamp().Logger()
}}
}
func NewLogger() zerolog.Logger {
return pool.Get().(zerolog.Logger)
}
func PutLogger(logger zerolog.Logger) {
pool.Put(logger)
}
注意,这里使用了标准库的sync.Pool
库,通过指定的Get方法得到日志实例,用完后再调用Put方法放回,已达到反复利用的效果,也可以避免在高并发时需要一次性大量调用new方法。
编写model模块
本项目使用sqlx和sqlex库进行数据库操作。
先拉取这两个库
go get -u github.com/jmoiron/sqlx
go get -u github.com/unrotten/sqlex
在模块model下,编写db.go
文件:
package model
import (
"fmt"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"github.com/sony/sonyflake"
"github.com/spf13/viper"
"github.com/unrotten/sqlex"
"log"
"time"
)
var (
DB *sqlx.DB
PSql sqlex.StatementBuilderType
IdFetcher *sonyflake.Sonyflake
)
// 初始化数据库连接
func init() {
// 获取数据库配置信息
user := viper.Get("storage.user")
password := viper.Get("storage.password")
host := viper.Get("storage.host")
port := viper.Get("storage.port")
dbname := viper.Get("storage.dbname")
// 连接数据库
psqlInfo := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
host, port, user, password, dbname)
DB = sqlx.MustOpen("postgres", psqlInfo)
if err := DB.Ping(); err != nil {
log.Fatalf("连接数据库失败:%s", err)
}
// 初始化sql构建器,指定format形式
PSql = sqlex.StatementBuilder.PlaceholderFormat(sqlex.Dollar)
// 初始化sonyflake
st := sonyflake.Settings{
StartTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.Local),
}
IdFetcher = sonyflake.NewSonyflake(st)
}
这里需要注意的是PSql = sqlex.StatementBuilder.PlaceholderFormat(sqlex.Dollar)
指定了构建sql后所使用的占位符$,后续构建sql都将统一通过PSql进行构建。sqlex库默认情况下,使用?作为占位符。sqlex库fork自squirrel库,主要用于sql的构建。sqlex库在原库的基础上增加了IF用法,可以通过条件判断是否需要构建指定的sql块。譬如绝大多数下,我们使用的where语句,可能需要先判断条件是否满足再决定要不要将sql拼接上去,这里IF用法就简略了if判断手动拼接的用法。另外sqlex库也可以通过RunWith(DB)
直接执行将要构建的sql语句。
初始化的IdFetcher使用了sonyflake库,通过雪花算法生成32位的id。
编写graphql.go文件:
此项目使用GraphQL API技术,通过graphql库实现。先拉取该库go get -u github.com/graphql-go/graphql
,在controller
目录下编写graphql.go
文件:
package controller
import (
"context"
"github.com/gin-gonic/gin"
"github.com/graphql-go/graphql"
"github.com/graphql-go/handler"
"net/http"
)
var (
schema graphql.Schema
queryType *graphql.Object
mutationType *graphql.Object
subscriptType *graphql.Object
)
type RequestOptions struct {
Query string `json:"query" url:"query" schema:"query"`
Variables map[string]interface{} `json:"variables" url:"variables" schema:"variables"`
OperationName string `json:"operationName" url:"operationName" schema:"operationName"`
}
func Register(e *gin.Engine) {
queryType = graphql.NewObject(graphql.ObjectConfig{Name: "Query", Fields: graphql.Fields{
"test": {
Name: "test",
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return "test", nil
},
Description: "test",
},
}})
mutationType = graphql.NewObject(graphql.ObjectConfig{Name: "Mutation", Fields: graphql.Fields{
"test": {
Name: "test",
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return "test", nil
},
Description: "test",
},
}})
subscriptType = graphql.NewObject(graphql.ObjectConfig{Name: "Subscription", Fields: graphql.Fields{
"test": {
Name: "test",
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return "test", nil
},
Description: "test",
},
}})
schemaConfig := graphql.SchemaConfig{
Query: queryType,
Mutation: mutationType,
Subscription: subscriptType,
}
var err error
schema, err = graphql.NewSchema(schemaConfig)
if err != nil {
panic(err)
}
h := handler.New(&handler.Config{
Schema: &schema,
Pretty: true,
GraphiQL: true,
Playground: false,
})
router := func(ctx *gin.Context) {
h.ContextHandler(context.Background(), ctx.Writer, ctx.Request)
}
// graphql的web界面,只有admin才能进入
e.GET("/graphql", router)
e.POST("/graphql", router)
e.OPTIONS("/graphql", router)
e.GET("/query", query)
e.OPTIONS("/query", query)
e.POST("/query", query)
}
func query(ctx *gin.Context) {
requestOption := &RequestOptions{}
_ = ctx.Bind(requestOption)
ctx.Set("operationName", requestOption.OperationName)
result := graphql.Do(graphql.Params{
Schema: schema,
RequestString: requestOption.Query,
VariableValues: requestOption.Variables,
OperationName: requestOption.OperationName,
})
ctx.JSON(http.StatusOK, result)
}
定义RequestOptions结构体,用于绑定从前端传入的graphql格式的输入。
定义schema,由三个空的Object组成。其中queryType用于查询,对应restful中的get;mutationType用于插入更新,对应restful中的post;subscription是长链接,具体使用时将使用websocket技术实现。
这里定义了两个gin的HandlerFunc,分别是router和query。router主要用于开发阶段对graphql的界面调试,上线后需关闭,query则负责真正和前端进行交互。
编写main.go文件
在cmd/hello-world-web
目录下,新建main.go
文件:
package main
import (
"context"
"fmt"
"github.com/gin-gonic/gin"
"github.com/unrotten/hello-world-web/setting"
"github.com/unrotten/hello-world-web/util"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
gin.SetMode(setting.RunMode)
engine := gin.New()
controller.Register(engine)
addr := ":" + setting.HttpPort
server := &http.Server{
Addr: addr,
Handler: engine,
MaxHeaderBytes: 1 << 20,
}
logger := util.NewLogger()
go func() {
logger.Info().Msg(fmt.Sprintf("server run on:%s", addr))
if err := server.ListenAndServe(); err != nil {
logger.Fatal().Caller().Err(err).Msg("server err")
}
}()
quit := make(chan os.Signal)
signal.Notify(quit, os.Interrupt)
<-quit
logger.Info().Msg("Shutdown Server ...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Fatal().Caller().Err(err).Msg("Server Shutdown")
}
logger.Info().Msg("Server exiting")
}
通过graphql.go文件中的Register方法,将路由注册到gin中。
启动后,进入graphql的调试界面,如图所示:
有疑问加站长微信联系(非本文作者)
![](https://static.golangjob.cn/static/img/footer.png?imageView2/2/w/280)