# miniblog **Repository Path**: funktest7ff/miniblog ## Basic Information - **Project Name**: miniblog - **Description**: Go 工程化学习 demo项目 - **Primary Language**: Go - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2024-08-01 - **Last Updated**: 2024-08-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: Go语言, Gin, gorm ## README ## miniblog 项目 ### 初始化 Go 项目 #### 编写HelloWorld ```shell # 搭建目录 mkdir -p api configs docs scripts # 编译 HelloWorld go build -o _output/miniblog -v cmd/miniblog/main.go # 执行程序 ./_output/miniblog ``` #### 程序实时加载、启动、构建 ```shell go install github.com/cosmtrek/air air ``` ### 搭建应用程序框架 miniblog 使用了 pflag、viper、cobra 来构建。具体步骤如下: ```shell go get github.com/spf13/pflag go get github.com/spf13/cobra go get go.uber.org/automaxprocs mkdir -p internal/miniblog # 编写代码 make # 编译 miniblog _output/miniblog -h # 打印 miniblog 使用帮助信息 _output/miniblog test # 指定命令行参数 ``` ### 读取配置 配置项少的时候(例如:5 个以内),可以从命令行选项中读取。参数较多的时候适合从配置文件读取。 不知道如何实现? - 学习并拷贝已有示例代码 -> 二次开发(优化、功能添加)。 ```shell go get github.com/spf13/viper make _output/miniblog -c configs/miniblog.yaml ``` ### 封装日志包 ```shell go get go.uber.org/zap mkdir -p internal/pkg/log ``` 一个日志包通常有 2 类 zapLogger 对象:一个全局对象、一个局部对象。全局对象方便我们通过 `log.Infow()` 这种方式来调用。 局部对象方便我们传入不同的参数,来创建一个自定义的 Logger。为了实现这个目标,我们通常需要实现 2 个函数: - `NewLogger(opts *Options) *zapLogger`: 创建一个自定义的 `*zapLogger`; - `Init(opts *Options)`:用来初始化全局的 Logger。 ### 给应用添加版本信息 Go 官方提供了一种更好的方式:通过 -ldflags -X importpath.name=value来给程序自动添加版本信息。 ```shell go get github.com/gosuri/uitable mkdir -p pkg/version ``` 通过 -ldflags -X "importpath.name=value" 构建参数将版本信息注入到 version 包中。 ```shell ## 指定应用使用的 version 包,会通过 `-ldflags -X` 向该包中指定的变量注入值 VERSION_PACKAGE=github.com/marmotedu/miniblog/pkg/version ## 定义 VERSION 语义化版本号 ifeq ($(origin VERSION), undefined) VERSION := $(shell git describe --tags --always --match='v*') endif ## 检查代码仓库是否是 dirty(默认dirty) GIT_TREE_STATE:="dirty" ifeq (, $(shell git status --porcelain 2>/dev/null)) GIT_TREE_STATE="clean" endif GIT_COMMIT:=$(shell git rev-parse HEAD) GO_LDFLAGS += \ -X $(VERSION_PACKAGE).GitVersion=$(VERSION) \ -X $(VERSION_PACKAGE).GitCommit=$(GIT_COMMIT) \ -X $(VERSION_PACKAGE).GitTreeState=$(GIT_TREE_STATE) \ -X $(VERSION_PACKAGE).BuildDate=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ') # 输出版本和 Git Commit make _output/miniblog -c configs/miniblog.yaml --version ``` ### 实现简单 Web 服务 ```shell go get github.com/gin-gonic/gin ``` ```shell curl http://127.0.0.1:8080/healthz ``` ### 添加 Web 中间件 ```shell go get github.com/google/uuid ``` #### 跨域 同源策略会阻止一个域的 javascript 脚本和另外一个域的内容进行交互; 所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol),主机(host)和端口号(port); 非同源限制: - 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB; - 无法接触非同源网页的 DOM; - 无法向非同源地址发送 AJAX 请求。 解决跨域问题的方法有多种,例如:CORS、Nginx 反向代理、JSONP 跨域等。 在 Go 后端服务开发中,通常可以使用 CORS 来解决跨域问题。 CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。 它允许浏览器向跨域服务器,发出 AJAX 请求,从而克服了 AJAX 只能同源使用的限制。 ##### 简单请求的 CORS 跨域处理 简单请求: 请求方法是 GET、HEAD 或者 POST,并且 HTTP 请求头中只有 Accept/Accept-Language/Content-Language/Last-Event-ID/Content-Type 6 种类型, 且 Content-Type 只能是 application/x-www-form-urlencoded, multipart/form-data 或着 text / plain 中的一个值。简单请求会在发送时自动在 HTTP 请求头加上 Origin 字段, 来标明当前是哪个源(协议 + 域名 + 端口),服务端来决定是否放行。 对于简单请求,浏览器直接发出 CORS 请求。具体来说,就是在头信息之中,增加一个 Origin 字段: ```markdown origin: https://wetv.vip ``` 服务器需要处理这个头部,并填充返回头 `Access-Control-Allow-Origin`: ```markdown access-control-allow-origin: https://wetv.vip ``` ##### 复杂请求的 CORS 跨域处理 复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为"预检"请求(preflight)。 "预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问请求能否安全送出的。 当后端收到预检请求后,可以设置跨域相关 Header 以完成跨域请求。支持的 Header 具体如下表所示: | 返回头 | 说明 | |----------------------------------|-------------------------------------------------------------------------------------------------| | Access-Control-Allow-Origin | 必选,设置允许访问的域名 | | Access-Control-Allow-Methods | 必选,逗号分隔的字符串,表明服务器支持的所有跨域请求的方法 | | Access-Control-Allow-Headers | 逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。如果浏览器请求包括 Access-Control-Request-Headers 字段,则此字段是必选的 | | Access-Control-Allow-Credentials | 可选,布尔值,默认是false,表示不允许发送 Cookie | | Access-Control-Max-Age | 指定本次预检请求的有效期,单位为秒。可以避免频繁的预检请求 | #### 添加优雅关停功能 1. 启动 HTTP /gRPC 服务或者其他异步任务等,以非阻塞方式启动,如果服务本身是阻塞方式,我们可以放在 goroutine 中启动; 2. 创建 os.Signal 类型的 channel,用来捕获应用程序关停信号; 3. 调用 signal.Notify 函数设置需要捕获的信号,需要设置为 syscall.SIGINT, syscall.SIGTERM 2 种信号; 4. 调用 <-quit 阻塞主程序; 5. 之后,如果系统收到 SIGINT 和 SIGTERM 信号,就会往 quit channel 中写入一条 os.Signal 类型的数据; 6. quit 读取到数据,解除阻塞状态,执行后续的清理工作。清理工作执行完成后,正常终止进程。清理工作根据业务逻辑可以执行不同的清理函数,例如:可以通过 net/http 包提供的 Shutdown 方法,关停 HTTP 服务。 ### 封装错误码 错误码: ```go var ( // OK 代表请求成功. OK = &Errno{HTTP: 200, Code: "", Message: "Success"} // InternalServerError 表示所有未知的服务器端错误. InternalServerError = &Errno{HTTP: 500, Code: "InternalError", Message: "Internal server error."} // ErrPageNotFound 表示路由不匹配错误. ErrPageNotFound = &Errno{HTTP: 404, Code: "ResourceNotFound.PageNotFound", Message: "Page not found."} ) ``` 解析错误码的方法: ```go // Errno 定义了 miniblog 使用的错误类型. type Errno struct { HTTP int Code string Message string } // Error 实现 error 接口中的 `Error` 方法. func (err *Errno) Error() string { return err.Message } // Decode 尝试从 err 中解析出业务错误码和错误信息. func Decode(err error) (int, string, string) { if err == nil { return OK.HTTP, OK.Code, OK.Message } switch typed := err.(type) { case *Errno: return typed.HTTP, typed.Code, typed.Message default: } // 默认返回未知错误码和错误信息. 该错误代表服务端出错 return InternalServerError.HTTP, InternalServerError.Code, InternalServerError.Message } ``` 通用写返回体方法 ```go type ErrResponse struct { // Code 指定了业务错误码. Code string `json:"code"` // Message 包含了可以直接对外展示的错误信息. Message string `json:"message"` } // WriteResponse 通用写返回体的方法 // WriteResponse 使用 errno.Decode 方法,根据错误类型,尝试从 err 中提取业务错误码和错误信息. func WriteResponse(c *gin.Context, err error, data interface{}) { if err != nil { hcode, code, message := errno.Decode(err) c.JSON(hcode, ErrResponse{ Code: code, Message: message, }) return } c.JSON(http.StatusOK, data) } ``` ```shell curl -v http://127.0.0.1:8080/v1/users ─╯ * Trying 127.0.0.1:8080... * Connected to 127.0.0.1 (127.0.0.1) port 8080 > GET /v1/users HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/8.6.0 > Accept: */* > < HTTP/1.1 404 Not Found < Access-Control-Allow-Origin: * < Cache-Control: no-cache, no-store, max-age=0, must-revalidate, value < Content-Type: application/json; charset=utf-8 < Expires: Thu, 01 Jan 1970 00:00:00 GMT < Last-Modified: Sat, 03 Aug 2024 13:50:38 GMT < X-Content-Type-Options: nosniff < X-Frame-Options: DENY < X-Request-Id: 277dc025-ea35-4384-9878-c437bb4369d8 < X-Xss-Protection: 1; mode=block < Date: Sat, 03 Aug 2024 13:50:38 GMT < Content-Length: 68 < * Connection #0 to host 127.0.0.1 left intact {"code":"ResourceNotFound.PageNotFound","message":"Page not found."} ``` ### 业务架构 #### Model层开发 根据数据库表生成 Model 文件 ```shell go install github.com/Shelnutt2/db2struct/cmd/db2struct go get gorm.io/gorm go get gorm.io/driver/mysql mkdir -p internal/pkg/model cd internal/pkg/model db2struct --gorm --no-json -H 127.0.0.1 -d miniblog -t user --package model --struct UserM -u root -p 'root' --target=user.go db2struct --gorm --no-json -H 127.0.0.1 -d miniblog -t post --package model --struct PostM -u root -p 'root' --target=post.go ``` #### Store层开发 通过 UserStore接口定义对 User 的 DAO 操作 通过 IStore接口定义对BizStore的创建,但是BizStore每次都被创建; IStore依赖 *gorm.DB 的创建,在 pkg/db下编写初始化代码 在 internal/miniblog/helper.go中编写初始化 gorm.DB的代码并初始化 Store 实现类单例的函数 在 internal/miniblog/miniblog.go中的 run 方法中调用上述初始化 Store 的方法 #### Biz层开发 ```shell go get github.com/jinzhu/copier ``` 通过 IBiz 接口定义对 Biz具体接口实现的创建 通过 UserBiz 接口定义对 User 的业务操作 #### Controller层开发 ```shell go get github.com/asaskevich/govalidator make _output/miniblog -c configs/miniblog.yaml curl -XPOST -H"Content-Type: application/json" -d'{"username":"root","password":"miniblog1234","nickname":"root","email":"nosbelm@qq.com","phone":"1818888xxxx"}' http://127.0.0.1:8080/v1/users ```