-
Notifications
You must be signed in to change notification settings - Fork 1
/
README.md
1434 lines (1065 loc) · 44.1 KB
/
README.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# go-base
[
`go-base` 是一个基于 [GoFrameV2](https://github.com/gogf/gf) 用来快速开发后端服务的脚手架, 目标使开发者只需关注业务逻辑的编写, 快速且规范地交付项目.
## 💌 Features
- 优化工程目录结构, 使支持多个可执行命令
- 多环境管理: 开发环境、测试环境、生产环境
- 编译的二进制文件可打印当前应用的版本信息
- 中间件统一拦截响应, 规范响应格式, 规范业务错误码
- 完善 HTTP 服务访问日志、HTTP 服务错误日志、SQL 日志、开发者打印的日志、其他可执行命令的日志配置
- 完整的增删改查接口示例和完善的开发流程文档, 帮助开发者快速上手
- 项目部署遵循不可变基础设施原则, 不论是传统单体部署还是容器云部署方式
- 通过 `Makefile` 管理项目: `make run`, `make build`, `make dao`, `make service` 等
- 适合个人开发者高质量完成项目, 也适合团队统一后端技术框架, 规范高效管理
## 🚀 Quick Start
### 安装
```sh
cd go-base
# 安装gf
make cli
```
> 请提前安装 Go 环境, 要求 Go 版本: `1.15+`
### 热更新(Live reload)
开发环境下使用.
```sh
cd go-base
# 运行 http-api
make run
# 运行 cli
make run.cli
```
> 默认加载配置文件: `manifest/config/config.yaml`
### 访问测试
```sh
$ curl -X GET -i 'localhost:9000/v1/demo/windvalley'
HTTP/1.1 200 OK
Content-Type: application/json
Server: GoFrame HTTP Server
Trace-Id: 506dccff4a08431731f5d0259180c3b8
Date: Sun, 12 Feb 2023 09:03:24 GMT
Content-Length: 130
{"code":"OK","message":"","traceid": "506dccff4a08431731f5d0259180c3b8","data":{"id":1,"fielda":"windvalley","created_at":"2008-08-08 08:08:08","updated_at":"2008-08-08 08:08:08"}}
```
### 编译二进制文件
```sh
cd go-base
# 编译 http-api
make build
# 编译 cli
make build.cli
```
会生成如下二进制文件:
```text
bin
├── darwin_amd64
│ └── http-api
│ └── go-base-cli
└── linux_amd64
└── http-api
└── go-base-cli
```
### 打印帮助信息
```sh
$ bin/darwin_amd64/http-api -h
USAGE
http-api [OPTION]
OPTION
-v, --version print version info
-c, --config config file (default config.yaml)
-h, --help more information about this command
EXAMPLE
Dev:
./http-api
Test:
./http-api -c config.test.yaml
or
GF_GCFG_FILE=config.test.yaml GF_GERROR_BRIEF=true ./http-api
Prod:
./http-api -c config.prod.yaml
or
GF_GCFG_FILE=config.prod.yaml GF_GERROR_BRIEF=true ./http-api
DESCRIPTION
An API server demo using GoFrame V2
Find more information at: https://github.com/windvalley/go-base
```
## 📄 Documentation
- [工程目录](#工程目录-)
- [环境管理](#环境管理-)
- [开发环境](#开发环境)
- [测试环境](#测试环境)
- [生产环境](#生产环境)
- [多命令管理](#多命令管理-)
- [目录设计](#目录设计)
- [配置文件](#配置文件)
- [错误码管理](#错误码管理-)
- [规范制定](#规范制定)
- [业务错误码](#业务错误码)
- [响应示例](#响应示例)
- [日志管理](#日志管理-)
- [HTTP 服务日志](#HTTP-服务日志)
- [1. HTTP 服务日志配置](#1-HTTP-服务日志配置)
- [2. 生成的日志示例](#2-生成的日志示例)
- [SQL 日志](#SQL-日志)
- [1. SQL 日志配置](#1-SQL-日志配置)
- [2. 生成的日志示例](#2-生成的日志示例)
- [开发者打印的通用日志](#开发者打印的通用日志)
- [1. 通用日志配置](#1-通用日志配置)
- [2. 如何打日志](#2-如何打日志)
- [3. 生成的日志示例](#3-生成的日志示例)
- [链路跟踪](#链路跟踪-)
- [版本管理](#版本管理-)
- [1. 写版本变更文档](#1-写版本变更文档)
- [2. 给项目仓库打 tag](#2-给项目仓库打-tag)
- [3. 使用 Makefile 编译](#3-使用-Makefile-编译)
- [4. 查看二进制文件版本信息](#4-查看二进制文件版本信息)
- [开发流程](#开发流程-)
- [1. 设计表结构, 创建物理表](#1-设计表结构-创建物理表)
- [2. 自动生成数据层相关代码](#2-自动生成数据层相关代码)
- [3. 编写 api 层代码](#3-编写-api-层代码)
- [4. 编写 model 层代码](#4-编写-model-层代码)
- [5. 编写 service 层代码](#5-编写-service-层代码)
- [6. 编写 controller 层代码](#6-编写-controller-层代码)
- [7. 路由注册](#7-路由注册)
- [8. 接口访问测试](#8-接口访问测试)
- [项目部署](#项目部署-)
- [Systemctl](#Systemctl)
- [Supervisor](#Supervisor)
- [Docker](#Docker)
- [优雅关闭测试](#优雅关闭测试)
- [使用 Makefile 管理项目](#使用-makefile-管理项目-)
- [变更项目名称](#变更项目名称-)
### 工程目录 [⌅](#-documentation)
```sh
├── CHANGELOG.md # 版本变更管理
├── Dockerfile # 用于制作容器镜像
├── Makefile # 用于项目管理
├── README.md # 项目文档
├── api # 对外接口定义: 对外提供服务的输入/输出数据结构定义, 路由path定义, 数据校验等
│ └── v1
│ └── demo.go
├── bin # make build 和 make build.cli 生成的二进制可执行文件所在目录
│ ├── darwin_amd64
│ │ ├── http-api
│ │ └── cli
│ └── linux_amd64
│ ├── http-api
│ └── cli
├── cmd # 项目的可执行文件入口
│ ├── http-api # API服务
│ │ └── http-api.go # 注意: 编译时会使用入口文件的名字作为二进制文件名称
│ └── cli # 项目的其他可执行服务, 比如可以是: 命令行工具或Daemon后台程序等和项目相关的辅助应用
│ └── cli.go
├── hack # 存放项目开发工具、脚本等内容. 例如: gf工具的配置, 各种shell/bat脚本等文件
│ └── config.yaml # gf 工具的配置文件, 比如 gf gen/gf build 等会使用这里的配置内容
│ └── change_project_name.sh # 将示例项目名称改成你自己的项目名称
├── internal
│ ├── cmd # 对应外层 cmd 目录
│ │ ├── apiserver # 对应 http-api, 命令配置, 路由注册等
│ │ │ └── apiserver.go
│ │ └── cli # 对应 cli, 命令配置等
│ │ └── cli.go
│ ├── codes # 业务错误码定义维护
│ │ ├── biz_codes.go
│ │ └── codes.go
│ ├── consts # 项目所有通用常量定义
│ │ └── consts.go
│ ├── controller # 对外接口实现: 接收/解析用户输入参数的入口/接口层
│ │ └── demo.go
│ ├── dao # 数据访问对象,这是一层抽象对象,用于和底层数据库交互,仅包含最基础的 CURD 方法. dao层通过框架的ORM抽象层组件与底层真实的数据库交互
│ │ ├── demo.go
│ │ └── internal
│ │ └── demo.go
│ ├── logic # 业务封装: 业务逻辑封装管理, 特定的业务逻辑实现和封装. 往往是项目中最复杂的部分. logic层的业务逻辑需要通过调用dao来实现数据的操作, 调用dao时需要传递do数据结构对象, 用于传递查询条件、输入数据. dao执行完毕后通过Entity数据模型将数据结果返回给service(logic)层
│ │ ├── logic.go
│ │ ├── demo # demo服务的具体实现
│ │ │ └── demo.go
│ │ └── middleware # 中间件
│ │ ├── accessuser.go
│ │ ├── middleware.go
│ │ ├── response.go # 统一拦截规范响应
│ │ └── traceid.go
│ ├── model # 数据结构管理模块, 管理数据实体对象, 以及输入与输出数据结构定义. 这里的model不仅负责维护数据实体对象(entity)结构定义, 也包括所有的输入/输出数据结构定义, 被api/dao/service共同引用
│ │ ├── demo.go # 输入/输出数据结构定义
│ │ ├── do # 领域对象: 用于dao数据操作中业务模型与实例模型转换. NOTE: 由工具维护, 不要手动修改
│ │ │ └── demo.go
│ │ └── entity # 数据模型: 是模型与数据集合的一对一关系, 通常和数据表一一对应. NOTE: 由工具维护, 不要手动修改
│ │ │ └── demo.go
│ ├── packed
│ │ └── packed.go
│ └── service # 业务接口: 用于业务模块解耦的接口定义层. 具体的接口实现在logic中进行注入. NOTE: 由工具维护, 不要手动修改
│ ├── demo.go
│ └── middleware.go
├── manifest # 交付清单: 包含应用配置文件, 部署文件等
│ ├── config # 配置文件存放目录, 可通过gf build/make build打包到二进制文件中
│ │ ├── config.prod.yaml # 生产环境
│ │ ├── config.test.yaml # 测试环境
│ │ └── config.yaml # 开发环境
│ ├── deploy # 和部署相关的文件
│ │ ├── kustomize # Kubernetes集群化部署的Yaml模板, 通过kustomize管理
│ │ │ ├── base
│ │ │ │ ├── deployment.yaml
│ │ │ │ ├── kustomization.yaml
│ │ │ │ └── service.yaml
│ │ │ └── overlays
│ │ │ └── develop
│ │ │ ├── configmap.yaml
│ │ │ ├── deployment.yaml
│ │ │ └── kustomization.yaml
│ │ ├── supervisor # 通过 supervisor 管理服务
│ │ │ ├── deploy.sh # 一键部署脚本
│ │ │ ├── http-api.ini # 本项目生产环境supervisor配置文件
│ │ │ └── go-base-api_test.ini # 本项目测试环境supervisor配置文件
│ │ └── systemctl # 通过systemctl管理服务
│ │ ├── deploy.sh # 一键部署脚本
│ │ ├── http-api.service # 生产环境服务文件
│ │ └── go-base-api_test.service # 测试环境服务文件
│ └── sql
│ └── gf2_demo.sql # 用于创建示例表的sql文件
├── resource # 静态资源文件: 这些文件往往可以通过资源打包/镜像编译的形式注入到发布文件中, 纯后端api服务一般用不到此目录
│ ├── i18n
│ ├── public
│ │ ├── html
│ │ ├── plugin
│ │ └── resource
│ │ ├── css
│ │ ├── image
│ │ └── js
│ └── template
└── utility # 通用工具类
├── accessuser.go
└── version.go
```
### 环境管理 [⌅](#-documentation)
#### 开发环境
配置文件: `manifest/config/config.yaml`
运行:
`make run` 或 `./http-api`
> 会默认加载配置文件 config.yaml
#### 测试环境
配置文件: `manifest/config/config.test.yaml`
运行:
- 通过环境变量指定配置文件: `GF_GCFG_FILE=config.test.yaml GF_GERROR_BRIEF=true ./http-api`
- 通过命令行参数指定配置文件: `./http-api -c config.test.yaml`
> NOTE:
>
> - 通过命令行参数指定配置文件优先于环境变量.
> - -c 参数指定的配置文件可以使用绝对路径, 如果不包含路径, 默认依次在如下路径搜索配置文件: `./`(二进制所在的当前目录) > `./config/` > `./manifest/config/`.
> - `GF_GERROR_BRIEF=true` 表示 HTTP 服务日志错误堆栈中不包含 gf 框架堆栈.
> - 配置文件在通过 `make build` 或 `make build.cli` 编译时已经打包到二进制文件中, 所以在部署时只需部署二进制文件即可.
#### 生产环境
配置文件: `manifest/config/config.prod.yaml`
运行:
同测试环境, 只不过指定的配置文件不同, 略.
### 多命令管理 [⌅](#-documentation)
#### 目录设计
举例:
- 命令 1: `cmd/http-api/http-api.go` -> `internal/cmd/apiserver/apiserver.go`
- 命令 2: `cmd/go-base-cli/go-base-cli.go` -> `internal/cmd/cli/cli.go`
#### 配置文件
默认不同命令在相同环境下使用同一个配置文件, 比如 `http-api` 和 `go-base-cli` 在开发环境下都使用 `manifest/config/config.yaml` 作为配置文件.
不过也可以使用各自独立的配置文件, 只需要在运行时通过环境变量或命令行参数指定需要使用的配置文件即可, 比如:
`./go-base-cli -c cli_config.yaml` 或
`GF_GCFG_FILE=cli_config.yaml ./go-base-cli`
### 错误码管理 [⌅](#-documentation)
#### 规范制定
- 统一响应格式
不论是正确还是错误响应, 响应体都统一使用如下格式:
```json
{
"code": "string",
"message": "string",
"traceid": "string",
"data": null
}
```
> 💡 **响应 header 中已经有了`Trace-Id`了, 为什么响应 json 中还要加一个`traceid` ?**
>
> 目的是在遇到错误问题进行排查时, 减少不必要的沟通成本, 毕竟很多用户容易忽略响应 header,
> 在响应体中直接体现 `traceid` 更直接. 这样在快速拿到用户反馈的 `traceid` 后,
> 我们就可以很快找到对应的日志从而高效解决问题了.
- 业务码
统一使用字符串表示, 如: `"code": "ValidationFailed"`
- HTTP 状态码
- 正确响应
- `200`: 成功的响应
- `202`: 部分成功的响应
- 客户端错误
- `401`: 未通过访问认证
- `403`: 请求的资源未获得授权
- `404`: 请求的资源不存在
- `400`: 其他所有客户端错误, 比如请求参数验证失败等
- 服务端错误
- `500`: 所有服务端错误
#### 业务错误码
请在 `internal/codes/biz_codes.go` 文件中维护业务错误码.
```go
package codes
// http status, bisiness code, message
var (
CodeOK = New(200, "OK", "")
CodePartSuccess = New(202, "PartSuccess", "part success")
CodePermissionDenied = New(401, "AuthFailed", "authentication failed")
CodeNotAuthorized = New(403, "NotAuthorized", "resource is not authorized")
CodeNotFound = New(404, "NotFound", "resource does not exist")
CodeValidationFailed = New(400, "ValidationFailed", "validation failed")
CodeNotAvailable = New(400, "NotAvailable", "not available")
CodeInternal = New(500, "InternalError", "an error occurred internally")
)
```
#### 响应示例
- 正确响应
```text
HTTP/1.1 200 OK
Content-Type: application/json
Server: GoFrame HTTP Server
Trace-Id: 10c9769ce5cf4117c19a595c2d781e94
Date: Wed, 08 Feb 2023 09:38:41 GMT
Content-Length: 34
{
"code": "OK",
"message": "",
"traceid": "10c9769ce5cf4117c19a595c2d781e94",
"data": null
}
```
- 401 错误
```text
HTTP/1.1 401 Unauthorized
Content-Type: application/json
Server: GoFrame HTTP Server
Trace-Id: a89b7652b1cf41170d6e5233fbb76a21
Date: Wed, 08 Feb 2023 09:34:56 GMT
Content-Length: 83
{
"code": "AuthFailed",
"message": "authentication failed",
"traceid": "a89b7652b1cf41170d6e5233fbb76a21",
"data": null
}
```
- 500 错误
```text
HTTP/1.1 500 Internal Server Error
Content-Type: application/json
Server: GoFrame HTTP Server
Trace-Id: 70cd58a9d8cf4117376a265eb84137e5
Date: Wed, 08 Feb 2023 09:37:45 GMT
Content-Length: 73
{
"code": "InternalError",
"message": "an error occurred internally",
"traceid": "70cd58a9d8cf4117376a265eb84137e5",
"data": null
}
```
### 日志管理 [⌅](#-documentation)
#### HTTP 服务日志
##### 1. HTTP 服务日志配置
```yaml
# manifest/config/config.yaml
server:
# 服务日志(包括访问日志和server错误日志)
logPath: "logs/" # 日志文件存储目录路径, 建议使用绝对路径. 默认为空, 表示关闭
logStdout: true # 日志是否输出到终端. 默认为true
errorStack: true # 当Server捕获到异常时是否记录堆栈信息到日志中. 默认为true
errorLogEnabled: true # 是否记录异常日志信息到日志中. 默认为true
errorLogPattern: "error-{Ymd}.log" # 异常错误日志文件格式. 默认为"error-{Ymd}.log"
accessLogEnabled: true # 是否记录访问日志(包含异常的访问日志). 默认为false
accessLogPattern: "access-{Ymd}.log" # 访问日志文件格式. 默认为"access-{Ymd}.log"
# 针对服务日志的扩展配置
logger:
file: "{Ymd}.log" # 这里只记录server启动过程中gf默认打印的日志, 默认 {Y-m-d}.log; 日志所在路径为server.logPath指定的目录
ctxKeys: ["user", "mail"] # 自动打印Context的指定变量到日志中. 默认为空
rotateExpire: "1d"
rotateBackupExpire: "30d"
rotateBackupLimit: 30
rotateCheckInterval: "1h"
```
##### 2. 生成的日志示例
```sh
$ curl -X GET 'localhost:9000/v1/demo' \
-H 'X-Consumer-Custom-ID: windvalley' \
-H 'X-Consumer-Username: windvalley@sre.im'
```
- 服务访问日志示例
```sh
# 普通格式
2023-02-08 16:50:51.992 {10fde08349cd4117115968787a401378} {windvalley, windvalley@sre.im} 401 "GET http localhost:9000 /v1/hello HTTP/1.1" 0.004, ::1, "", "PostmanRuntime/7.28.0"
# json格式
{"Time":"2023-02-08 16:53:13.118","TraceId":"a8b1bf5f6acd41177931ba72f7411788","CtxStr":"windvalley, windvalley@sre.im","Level":"","Content":"401 \"GET http localhost:9000 /v1/hello HTTP/1.1\" 0.002, ::1, \"\", \"PostmanRuntime/7.28.0\""}
```
- 服务错误日志示例
```sh
# 普通格式
2023-02-08 16:55:25.984 {2068374f89cd41170d329c50fe5a5fc8} {windvalley, windvalley@sre.im} 401 "GET http localhost:9000 /v1/hello HTTP/1.1" 0.003, ::1, "", "PostmanRuntime/7.28.0", 0, "resource is not authorized", "{Code:NotAuthorized HttpCode:401}"
Stack:
1. resource is not authorized: some error
1). go-base/internal/controller.(*cHello).Hello
/Users/xg/github/go-base/internal/controller/hello.go:25
2). go-base/internal/logic/middleware.(*sMiddleware).ResponseHandler
/Users/xg/github/go-base/internal/logic/middleware/response.go:16
3). go-base/internal/logic/middleware.(*sMiddleware).AccessUser
/Users/xg/github/go-base/internal/logic/middleware/accessuser.go:25
4). go-base/internal/logic/middleware.(*sMiddleware).TraceID
/Users/xg/github/go-base/internal/logic/middleware/traceid.go:27
2. some error
# json格式
{"Time":"2023-02-08 16:54:28.757","TraceId":"18323afc7bcd411710d9f134cc2ec9d5","CtxStr":"windvalley, windvalley@sre.im","Level":"ERRO","Content":"401 \"GET http localhost:9000 /v1/hello HTTP/1.1\" 0.003, ::1, \"\", \"PostmanRuntime/7.28.0\", 0, \"resource is not authorized\", \"{Code:NotAuthorized HttpCode:401}\"\nStack:\n1. resource is not authorized: some error\n 1). go-base/internal/controller.(*cHello).Hello\n /Users/xg/github/go-base/internal/controller/hello.go:25\n 2). go-base/internal/logic/middleware.(*sMiddleware).ResponseHandler\n /Users/xg/github/go-base/internal/logic/middleware/response.go:16\n 3). go-base/internal/logic/middleware.(*sMiddleware).AccessUser\n /Users/xg/github/go-base/internal/logic/middleware/accessuser.go:25\n 4). go-base/internal/logic/middleware.(*sMiddleware).TraceID\n /Users/xg/github/go-base/internal/logic/middleware/traceid.go:27\n2. some error\n"}
```
#### SQL 日志
##### 1. SQL 日志配置
```yaml
# manifest/config/config.yaml
# doc: https://goframe.org/pages/viewpage.action?pageId=1114245
database:
# sql日志
logger:
path: "logs/"
file: "sql-{Ymd}.log"
level: "all"
stdout: true
ctxKeys: ["user", "mail"]
rotateExpire: "1d"
rotateBackupExpire: "30d"
rotateBackupLimit: 30
rotateCheckInterval: "1h"
```
##### 2. 生成的日志示例
```sh
# 普通格式
2023-02-12 16:52:32.330 [DEBU] {508ad625b3074317ec81cd791f1a5993} {windvalley, windvalley@sre.im} [ 2 ms] [default] [gf2_demo] [rows:5 ] SHOW FULL COLUMNS FROM `demo`
2023-02-12 16:52:32.331 [DEBU] {508ad625b3074317ec81cd791f1a5993} {windvalley, windvalley@sre.im} [ 1 ms] [default] [gf2_demo] [rows:1 ] SELECT * FROM `demo` WHERE `fielda`='windvalley' LIMIT 1
# json格式
{"Time":"2023-02-12 16:55:02.420","TraceId":"28a0d817d6074317a9e647156d712d81","CtxStr":"windvalley, windvalley@sre.im","Level":"DEBU","Content":"[ 3 ms] [default] [gf2_demo] [rows:5 ] SHOW FULL COLUMNS FROM `demo`"}
{"Time":"2023-02-12 16:55:02.421","TraceId":"28a0d817d6074317a9e647156d712d81","CtxStr":"windvalley, windvalley@sre.im","Level":"DEBU","Content":"[ 1 ms] [default] [gf2_demo] [rows:1 ] SELECT * FROM `demo` WHERE `fielda`='windvalley' LIMIT 1"}
```
#### 开发者打印的通用日志
##### 1. 通用日志配置
```yaml
# manifest/config/config.yaml
logger:
path: "logs/" # 日志文件目录, 如果为空, 表示不记录到文件; 建议目录和server.logPath保持一致
file: "{Ymd}.log" # 日志文件格式. 默认为 {Y-m-d}.log; 建议和server.logger.file保持一致
level: "all" # DEBU < INFO < NOTI < WARN < ERRO < CRIT, 也支持ALL, DEV, PROD常见部署模式配置名称. level配置项字符串不区分大小写
stStatus: 0 # 是否打印错误堆栈(1: enabled - default; 0: disabled). 如果开启, 使用g.Log().Error 将会打印错误堆栈
ctxKeys: ["user", "mail"] # 自动打印Context的变量到日志中. 默认为空
stdout: true # 日志是否同时输出到终端. 默认true
stdoutColorDisabled: false # 关闭终端的颜色打印. 默认false
writerColorEnable: false # 日志文件是否带上颜色. 默认false, 表示不带颜色
rotateExpire: "1d" # 多长时间切分一次日志
rotateBackupExpire: "30d" # 删除超过多长时间的切分文件, 默认为0, 表示不备份, 切分则删除. 如果启用按时间备份, rotateBackupLimit 必须设置为一个相对较大的数
rotateBackupLimit: 30 # 最多保留多少个切分文件, 但rotateBackupExpire的配置优先. 默认为0, 表示不备份, 切分则删除. 可以不设置rotateBackupExpire
rotateCheckInterval: "1h" # 滚动切分的时间检测间隔, 一般不需要设置. 默认为1小时
format: "" # "json" or other, 也对server服务日志生效
# 为子项目go-base-cli配置独立的logger
cli:
path: "logs/"
file: "cli-{Ymd}.log"
level: "all"
stStatus: 1
stdout: true
stdoutColorDisabled: false
writerColorEnable: false
rotateExpire: "1d"
rotateBackupExpire: "30d"
rotateBackupLimit: 30
rotateCheckInterval: "1h"
format: ""
```
##### 2. 如何打日志
```go
// go-base-api的日志
g.Log().Info(ctx, "hello world")
g.Log().Errorf(ctx, "hello %s", "world")
// go-base-cli的日志
g.Log("cli").Debug(ctx, "hello world")
g.Log("cli").Warningf(ctx, "hello %s", "world")
```
##### 3. 生成的日志示例
```sh
# 普通格式
2023-02-08 17:02:31.906 [INFO] {389b4e7aeccd41175dd0bc18211c2519} {windvalley, windvalley@sre.im} /Users/xg/github/go-base/internal/controller/hello.go:33: hello world
2023-02-08 17:02:31.906 [ERRO] {389b4e7aeccd41175dd0bc18211c2519} {windvalley, windvalley@sre.im} /Users/xg/github/go-base/internal/controller/hello.go:34: hello world
# json格式
{"Time":"2023-02-08 17:04:08.957","TraceId":"d0e7f61203ce41171374033689322f91","CtxStr":"windvalley, windvalley@sre.im","Level":"INFO","CallerPath":"/Users/xg/github/go-base/internal/controller/hello.go:33:","Content":"hello world"}
{"Time":"2023-02-08 17:04:08.957","TraceId":"d0e7f61203ce41171374033689322f91","CtxStr":"windvalley, windvalley@sre.im","Level":"ERRO","CallerPath":"/Users/xg/github/go-base/internal/controller/hello.go:34:","Content":"hello world"}
```
### 链路跟踪 [⌅](#-documentation)
- 用于链路跟踪的响应 Header 为: `Trace-Id`, 会优先使用客户端传递的请求 Header `Trace-Id` 的值, 如果不存在会自动生成. 为了便于用户查看`Trace-Id`, 也在响应 json 中加入了 `traceid` 字段.
- 服务内部如果需要调用其他服务的接口, 请使用 `g.Client()`, 因为他会给请求自动注入`Trace-Id`, 这样不同 API 服务之间的日志就可以通过 `Trace-Id` 串起来了.
> 参考: https://goframe.org/pages/viewpage.action?pageId=49745257
### 版本管理 [⌅](#-documentation)
#### 1. 写版本变更文档
`vi CHANGELOG.md`
```text
## v0.3.0
### Added
- xxx
- xxx
### Changed
- xxx
- xxx
### Fixed
- xxx
- xxx
```
#### 2. 给项目仓库打 tag
```sh
git tag v0.3.0
git push --tags
```
#### 3. 使用 Makefile 编译
- gf 工具配置(`hack/config.yaml`)
```yaml
gfcli:
# doc: https://goframe.org/pages/viewpage.action?pageId=1115788
build:
path: "./bin" # 编译生成的二进制文件的存放目录. 生成的二进制名称默认与程序入口go文件同名
arch: "amd64"
system: "linux,darwin"
packSrc: "manifest/config" # 将项目需要的配置文件打包进二进制, 这样项目部署的时候就可以不用拷贝配置文件了
extra: ""
# 编译时的内置变量可以在运行时通过gbuild包获取, 比如: utility/version.go
varMap:
# NOTE:
# 1) `version` was generated by `make build`, Do Not Edit
# 2) you should manage versions by `git tag vX.X.X`
version: v0.3.0
```
- 编译
```sh
# For http-api
make build
# For cli
make build.cli
```
#### 4. 查看二进制文件版本信息
```sh
$ bin/darwin_amd64/http-api -v
输出:
App Version: v0.3.0-7-g1898e82
Git Commit: 2023-02-08 14:55:39 1898e82dbcb4c2e8a091eb12fc96ead2f04f5993
Build Time: 2023-02-08 15:31:20
Go Version: go1.17.6
GF Version: v2.3.1
```
### 开发流程 [⌅](#-documentation)
#### 1. 设计表结构, 创建物理表
- 设计表结构
```sql
-- manifest/sql/gf2_demo.sql
-- Create demo database
CREATE DATABASE IF NOT EXISTS `gf2_demo`;
USE `gf2_demo`;
-- Create demo table
DROP TABLE IF EXISTS `demo`;
CREATE TABLE `demo`
(
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'ID',
`fielda` varchar(45) NOT NULL COMMENT 'Field demo',
`fieldb` varchar(45) NOT NULL COMMENT 'Private field demo',
`created_at` datetime DEFAULT NULL COMMENT 'Created Time',
`updated_at` datetime DEFAULT NULL COMMENT 'Updated Time',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_fielda` (`fielda`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
```
- 创建物理表
```sh
$ mysql -uroot -p'123456' < manifest/sql/demo.sql
```
#### 2. 自动生成数据层相关代码
- gf 工具配置
```yaml
# hack/config.yaml
gfcli:
gen:
# doc: https://goframe.org/pages/viewpage.action?pageId=3673173
dao:
- link: "mysql:root:123456@tcp(127.0.0.1:3306)/gf2_demo"
tables: "" # 指定当前数据库中需要执行代码生成的数据表, 多个以逗号分隔. 如果为空, 表示数据库的所有表都会生成. 默认为空
descriptionTag: true # 用于指定是否为数据模型结构体属性增加desription的标签, 内容为对应的数据表字段注释. 默认 false
noModelComment: true # 用于指定是否关闭数据模型结构体属性的注释自动生成, 内容为数据表对应字段的注释. 默认 false
jsonCase: "snake" # 指定model中生成的数据实体对象中json标签名称规则. 默认 CamelLower
clear: true # 自动删除数据库中不存在对应数据表的本地dao/do/entity代码文件, 默认 false. 线上环境应设置为fasle
```
- 自动生成 `internal/dao`, `internal/model/do`, `internal/model/entity`
```sh
$ make dao
```
#### 3. 编写 api 层代码
位置: `api/v1/`
定义业务侧数据结构, 提供对外接口的输入/输出数据结构, 定义访问路由 path, 请求方法, 数据校验, api 文档等.
示例:
```go
// api/v1/demo.go
type DemoCreateReq struct {
g.Meta `path:"/demo" method:"post" tags:"DemoService" summary:"Create a demo record"`
Fielda string `p:"fileda" v:"required|passport|length:4,30"`
Fieldb string `p:"filedb" v:"required|length:10,30"`
}
type DemoCreateRes struct {
ID uint `json:"id"`
}
```
#### 4. 编写 model 层代码
位置: `internal/model/`
定义数据侧数据结构,提供对内的数据处理的输入/输出数据结构.
在 GoFrame 框架规范中, 这部分输入输出模型名称以 `XxxInput` 和 `XxxOutput` 格式命名, 需要在 `internal/model` 目录下创建文件.
示例:
```go
// internal/model/demo.go
type DemoCreateInput struct {
Fielda string
Fieldb string
}
type DemoCreateOutput struct {
ID uint
}
```
> 参考: https://goframe.org/pages/viewpage.action?pageId=7295964
#### 5. 编写 service 层代码
##### a. 编写具体的业务实现(`internal/logic/`)
调用数据访问层(`internal/dao/`), 编写具体的业务逻辑. 这里是业务逻辑的重心, 绝大部分的业务逻辑都应该在这里编写.
示例:
```go
// internal/logic/demo/demo.go
import (
"go-base/internal/dao"
"go-base/internal/model"
"go-base/internal/model/do"
"go-base/internal/model/entity"
)
type sDemo struct{}
func New() *sDemo {
return &sDemo{}
}
func (s *sDemo) Create(ctx context.Context, in model.DemoCreateInput) (*model.DemoCreateOutput, error) {
notFound, err := s.FieldaNotFound(ctx, in.Fielda)
if err != nil {
return nil, err
}
if !notFound {
err1 := gerror.WrapCode(codes.CodeNotAvailable, fmt.Errorf("fielda '%s' already exists", in.Fielda))
return nil, err1
}
id, err := dao.Demo.Ctx(ctx).Data(in).InsertAndGetId()
if err != nil {
return nil, err
}
return &model.DemoCreateOutput{
ID: uint(id),
}, nil
}
```
##### b. 自动生成 service 接口代码(`internal/service/`)
```sh
$ make service
```
##### c. 将业务实现注入到服务接口(依赖注入)
示例:
```go
// internal/logic/demo/demo.go
import "go-base/internal/service"
type sDemo struct{}
func init() {
service.RegisterDemo(New())
}
```
##### d. 程序启动时自动注册服务
在程序入口文件 `cmd/http-api/http-api.go` 中导入 logic 包.
示例:
```go
// cmd/http-api/http-api.go
package main
import _ "go-base/internal/logic"
```
> 参考: https://goframe.org/pages/viewpage.action?pageId=49770772
#### 6. 编写 controller 层代码
位置: `internal/controller/`
解析 api 层(`api/v1/`)定义的业务侧用户输入数据结构, 组装为 model 层(`internal/model/`)定义的数据侧输入数据结构实例, 调用 `internal/service/` 层的服务, 最后直接将结果或错误 return 即可(响应中间件会统一拦截处理, 按规范响应用户).
示例:
```go
// internal/controller/demo.go
import "go-base/internal/service"
var (
Demo = cDemo{}
)
type cDemo struct{}
func (c *cDemo) Create(ctx context.Context, req *v1.DemoCreateReq) (*v1.DemoCreateRes, error) {
data := model.DemoCreateInput{
Fielda: req.Fielda,
Fieldb: req.Fieldb,
}
res, err := service.Demo().Create(ctx, data)
if err != nil {
return nil, err
}
return &v1.DemoCreateRes{ID: res.ID}, err
}
```
#### 7. 路由注册
位置: `internal/cmd/apiserver/`
路由注册, 调用 controller 层(`internal/controller/`), 对外暴露接口.
示例:
```go
// internal/cmd/apiserver/apiserver.go
import "go-base/internal/controller"
var (
Main = gcmd.Command{
Func: func(ctx context.Context, parser *gcmd.Parser) (err error) {
s := g.Server()
s.Group("/v1", func(group *ghttp.RouterGroup) {
group.Bind(
// 对外暴露的接口路由集合: 是 /v1 和 api/v1/demo.go 文件下的所有DemoXxxReq结构体定义的 path 的组合
controller.Demo,
)
})
s.Run()
return nil
},
}
)
```
#### 8. 接口访问测试
```sh
# 运行
$ make run
# 访问
$ curl -X POST -i 'localhost:9000/v1/demo' -d \
'{
"fielda": "foobar",
"fieldb": "LJIYdjsvt83l"
}'
# 输出:
HTTP/1.1 200 OK
Content-Type: application/json
Server: GoFrame HTTP Server
Trace-Id: 0862d01e5aa64317c7fae45b326dabd1
Date: Tue, 14 Feb 2023 09:19:52 GMT
Content-Length: 88
{"code":"OK","message":"","traceid":"0862d01e5aa64317c7fae45b326dabd1","data":{"id":17}}
```
### 项目部署 [⌅](#-documentation)
#### Systemctl
1. 相关的配置文件及脚本
- 生产环境 systemctl 服务配置文件: `manifest/deploy/systemctl/http-api.service`
- 测试环境 systemctl 服务配置文件: `manifest/deploy/systemctl/go-base-api_test.service`
- 部署脚本: `manifest/deploy/systemctl/deploy.sh`
2. 设置目标服务器(修改 `deploy.sh` 脚本)
```sh
# 目标服务器, 请提前配置发布机到目标服务器之间的ssh key信任
deploy_server="go-base.sre.im"
# 用于连接目标服务器的用户名
deploy_user="vagrant"
# 项目部署目录
deploy_dir=/app/$project_name
```
3. 执行部署
```sh
# 部署测试环境
$ ./manifest/deploy/systemctl/deploy.sh test
# 部署生产环境
$ ./manifest/deploy/systemctl/deploy.sh prod
```
4. 验证
首先登录到目标服务器.