This commit is contained in:
zhuyasen 2022-09-19 23:24:11 +08:00
parent 1dab814b5b
commit 083a0499c1
296 changed files with 38728 additions and 3 deletions

9
.gitignore vendored
View File

@ -4,6 +4,7 @@
*.dll
*.so
*.dylib
cmd/sponge/sponge
# Test binary, built with `go test -c`
*.test
@ -12,4 +13,10 @@
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
vendor/
# idea
.idea
*.iml
*.ipr
*.iws

298
.golangci.yml Normal file
View File

@ -0,0 +1,298 @@
# This file configures github.com/zhufuyi/pkg.
run:
# timeout for analysis, e.g. 30s, 5m, default is 1m
timeout: 2m
# default concurrency is available CPU number
concurrency: 4
# include test files or not, default is true
tests: false
# which dirs to skip: issues from them won't be reported;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but default dirs are skipped independently
# from this option's value (see skip-dirs-use-default).
skip-dirs:
- pkg
- docs
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
- _test.go
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# list of build tags, all linters use it. Default is empty list.
build-tags:
- mytag
# default is true. Enables skipping of directories:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs-use-default: true
linters:
# please, do not use `enable-all`: it's deprecated and will be removed soon.
# inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
disable-all: true
enable:
#- bodyclose
- deadcode
- depguard
- dogsled
#- dupl
- errcheck
- gochecknoinits
- goconst
- gocyclo
- gofmt
- goimports
- golint
#- goprintffuncname
#- gosec
- gosimple
- govet
#- ineffassign
- interfacer
- lll
- misspell
- rowserrcheck
- structcheck
- typecheck
- unconvert
#- unparam
- unused
- varcheck
- whitespace
- staticcheck
linters-settings:
dogsled:
# checks assignments with too many blank identifiers; default is 2
max-blank-identifiers: 2
dupl:
# tokens count to trigger issue, 150 by default
threshold: 100
errcheck:
# report about not checking of errors in type assertions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
# [deprecated] comma-separated list of pairs of the form pkg:regex
# the regex is used to ignore names within pkg. (default "fmt:.*").
# see https://github.com/kisielk/errcheck#the-deprecated-method for details
ignore: fmt:.*,io/ioutil:^Read.*
# path to a file containing a list of functions to exclude from checking
# see https://github.com/kisielk/errcheck#excluding-functions for details
# exclude: /path/to/file.txt
funlen:
lines: 60
statements: 40
gocognit:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 10
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 20
godox:
# report any comments starting with keywords, this is useful for TODO or FIXME comments that
# might be left in the code accidentally and should be resolved before merging
keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting
- NOTE
- OPTIMIZE # marks code that should be optimized before merging
- HACK # marks hack-arounds that should be removed before merging
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
goimports:
# put imports beginning with prefix after 3rd-party packages;
# it's a comma-separated list of prefixes
local-prefixes: sponge
golint:
# minimal confidence for issues, default is 0.8
min-confidence: 0.8
gomnd:
settings:
mnd:
# the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description.
checks: argument,case,condition,operation,return,assign
govet:
# report about shadowed variables
check-shadowing: true
# settings per analyzer
settings:
printf: # analyzer name, run `go tool vet help` to see all analyzers
funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf
- (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf
# enable or disable analyzers by name
enable:
- atomicalign
enable-all: false
disable:
- shadow
disable-all: false
depguard:
list-type: blacklist
include-go-root: false
#packages:
# - github.com/user/name
#packages-with-error-message:
# specify an error message to output when a blacklisted package is used
# - github.com/user/name: "logging is allowed only by logutils.Log"
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 200
# tab width in spaces. Default to 1.
tab-width: 1
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
ignore-words:
- someword
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 30
prealloc:
# XXX: we don't recommend using this linter before doing performance profiling.
# For most programs usage of prealloc will be a premature optimization.
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# True by default.
simple: true
range-loops: true # Report preallocation suggestions on range loops, true by default
for-loops: false # Report preallocation suggestions on for loops, false by default
#rowserrcheck:
# packages:
# - github.com/user/name
unparam:
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
whitespace:
multi-if: false # Enforces newlines (or comments) after every multi-line if statement
multi-func: false # Enforces newlines (or comments) after every multi-line function signature
wsl:
# If true append is only allowed to be cuddled if appending value is
# matching variables, fields or types on line above. Default is true.
strict-append: true
# Allow calls and assignments to be cuddled as long as the lines have any
# matching variables, fields or types. Default is true.
allow-assign-and-call: true
# Allow multiline assignments to be cuddled. Default is true.
allow-multiline-assign: true
# Allow declarations (var) to be cuddled.
allow-cuddle-declarations: false
# Allow trailing comments in ending of blocks
allow-trailing-comment: false
# Force newlines in end of case at this limit (0 = never).
force-case-trailing-whitespace: 0
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
- abcdef
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gocyclo
- errcheck
- dupl
- gosec
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via "nolint" comments.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude lll issues for long lines with go:generate
- linters:
- lll
source: "^//go:generate "
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: false
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-issues-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
new: false
# Show only new issues created after git revision `REV`
new-from-rev: ""
service:
golangci-lint-version: 1.48.0 # use the fixed version to not introduce new linters unexpectedly

140
Makefile Normal file
View File

@ -0,0 +1,140 @@
SHELL := /bin/bash
PROJECT_NAME := "github.com/zhufuyi/sponge"
PKG := "$(PROJECT_NAME)"
PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/)
GO_FILES := $(shell find . -name '*.go' | grep -v /vendor/ | grep -v _test.go)
.PHONY: init
# init env
init:
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.0
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.10.0
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.10.0
go install github.com/envoyproxy/protoc-gen-validate@v0.6.7
go install github.com/mohuishou/protoc-gen-go-gin@v0.1.0
go install github.com/srikrsna/protoc-gen-gotag@v0.6.2
go install github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc@v1.5.1
go install github.com/golang/mock/mockgen@v1.6.0
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.49.0
go install github.com/swaggo/swag/cmd/swag@v1.8.6
go install github.com/ofabry/go-callvis@v0.6.1
.PHONY: ci-lint
# make ci-lint
ci-lint:
golangci-lint run ./...
.PHONY: build
# make build, Build the binary file
build:
@cd cmd/sponge && go build
@echo "build finished, binary file in path 'cmd/sponge'"
.PHONY: run
# make run, run app
run:
@bash scripts/run.sh
.PHONY: dep
# make dep Get the dependencies
dep:
@go mod download
.PHONY: fmt
# make fmt
fmt:
@gofmt -s -w .
.PHONY: test
# make test
test:
go test -cover ./... | grep -v vendor;true
go vet ./... | grep -v vendor;true
go test -short ${PKG_LIST}
.PHONY: cover
# make cover
cover:
go test -short -coverprofile coverage.txt -covermode=atomic ${PKG_LIST}
go tool cover -html=coverage.txt
.PHONY: docker
# generate docker image
docker:
docker build -t sponge:latest -f Dockerfile .
.PHONY: clean
# make clean
clean:
@-rm -vrf sponge
@-rm -vrf cover.out
@-rm -vrf coverage.txt
@go mod tidy
@echo "clean finished"
.PHONY: docs
# gen swagger doc
docs:
@swag init -g cmd/sponge/main.go
@echo "see docs by: http://localhost:8080/swagger/index.html"
.PHONY: graph
# make graph 生成交互式的可视化Go程序调用图生成完毕后会在浏览器自动打开
graph:
@echo "generating graph ......"
@go-callvis github.com/zhufuyi/sponge
.PHONY: mockgen
# make mockgen gen mock file
mockgen:
cd ./internal && for file in `egrep -rnl "type.*?interface" ./repository | grep -v "_test" `; do \
echo $$file ; \
cd .. && mockgen -destination="./internal/mock/$$file" -source="./internal/$$file" && cd ./internal ; \
done
.PHONY: proto
# generate proto struct only
proto:
@bash scripts/protoc.sh
.PHONY: proto-doc
# generate proto doc
proto-doc:
@bash scripts/proto-doc.sh
# show help
help:
@echo ''
@echo 'Usage:'
@echo ' make [target]'
@echo ''
@echo 'Targets:'
@awk '/^[a-zA-Z\-\_0-9]+:/ { \
helpMessage = match(lastLine, /^# (.*)/); \
if (helpMessage) { \
helpCommand = substr($$1, 0, index($$1, ":")-1); \
helpMessage = substr(lastLine, RSTART + 2, RLENGTH); \
printf "\033[36m %-22s\033[0m %s\n", helpCommand,helpMessage; \
} \
} \
{ lastLine = $$0 }' $(MAKEFILE_LIST)
.DEFAULT_GOAL := all

View File

@ -1,2 +1,65 @@
# sponge
microservice framework.
## sponge
sponge 是一个微服务框架支持http和grpc及服务治理结合[goctl](https://github.com/zhufuyi/goctl)工具自动生成框架代码。
功能:
- web框架 [gin](https://github.com/gin-gonic/gin)
- rpc框架 [grpc](https://github.com/grpc/grpc-go)
- 配置文件解析 [viper](https://github.com/spf13/viper)
- 日志 [zap](go.uber.org/zap)
- 数据库组件 [gorm](gorm.io/gorm)
- 缓存组件 [go-redis](github.com/go-redis/redis)
- 生成文档 [swagger](github.com/swaggo/swag)
- 校验器 [validator](github.com/go-playground/validator)
- 链路跟踪 [opentelemetry](go.opentelemetry.io/otel)
- 指标采集 [prometheus](github.com/prometheus/client_golang/prometheus)
- 限流 [ratelimiter](golang.org/x/time/rate)
- 熔断 [hystrix](github.com/afex/hystrix-go)
- 包管理工具 [go modules](https://github.com/golang/go/wiki/Modules)
- 性能分析 [go profile](https://go.dev/blog/pprof)
- 代码检测 [golangci-lint](https://github.com/golangci/golangci-lint)
<br>
### 目录结构
目录结构遵循[golang-standards/project-layout](https://github.com/golang-standards/project-layout)。
```
├── cmd # 应用程序的目录
├── config # 配置文件目录
├── docs # 设计和用户文档
├── internal # 私有应用程序和库代码
│ ├── cache # 基于业务封装的cache
│ ├── dao # 数据访问
│ ├── ecode # 自定义业务错误码
│ ├── handler # http的业务功能实现
│ ├── model # 数据库 model
│ ├── routers # http 路由
│ ├── server # 服务入口包括http和grpc服务入口
│ └── service # grpc的业务功能实现
├── pkg # 外部应用程序可以使用的库代码
├── scripts # 存放用于执行各种构建,安装,分析等操作的脚本
├── third_party # 外部辅助工具,分叉代码和其他第三方工具
├── test # 额外的外部测试应用程序和测试数据
├── build # 打包和持续集成
└── deployments # IaaS、PaaS、系统和容器编排部署配置和模板
```
<br>
### 运行
根据配置文件设置是否开启服务治理功能,例如链路跟踪、指标采集、限流、性能分析。
启动web服务
> run.sh
在浏览器打开 `http://ip:port/swagger/index.html` 可以看到接口文档可以把swagger的json文档导入 [yapi](https://github.com/YMFE/yapi) 测试。
每次更新都要执行swag init并且重启服务生效。
<br>

263
api/types/types.pb.go Normal file
View File

@ -0,0 +1,263 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.0
// protoc v3.20.1
// source: api/types/types.proto
package types
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Column struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // 列名
Exp string `protobuf:"bytes,2,opt,name=exp,proto3" json:"exp,omitempty"` // 表达式,值为空时默认为=,有=、!=、>、>=、<、<=、like七种类型
Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"` // 列值
Logic string `protobuf:"bytes,4,opt,name=logic,proto3" json:"logic,omitempty"` // 逻辑类型值为空时默认为and有&(and)、||(or)两种类型
}
func (x *Column) Reset() {
*x = Column{}
if protoimpl.UnsafeEnabled {
mi := &file_api_types_types_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Column) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Column) ProtoMessage() {}
func (x *Column) ProtoReflect() protoreflect.Message {
mi := &file_api_types_types_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Column.ProtoReflect.Descriptor instead.
func (*Column) Descriptor() ([]byte, []int) {
return file_api_types_types_proto_rawDescGZIP(), []int{0}
}
func (x *Column) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Column) GetExp() string {
if x != nil {
return x.Exp
}
return ""
}
func (x *Column) GetValue() string {
if x != nil {
return x.Value
}
return ""
}
func (x *Column) GetLogic() string {
if x != nil {
return x.Logic
}
return ""
}
type Params struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` // 页码从0开始
Limit int32 `protobuf:"varint,2,opt,name=limit,proto3" json:"limit,omitempty"` // 每页行数
Sort string `protobuf:"bytes,3,opt,name=sort,proto3" json:"sort,omitempty"` // 排序字段,多列排序用逗号分隔
Columns []*Column `protobuf:"bytes,4,rep,name=columns,proto3" json:"columns,omitempty"` // 查询条件
}
func (x *Params) Reset() {
*x = Params{}
if protoimpl.UnsafeEnabled {
mi := &file_api_types_types_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Params) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Params) ProtoMessage() {}
func (x *Params) ProtoReflect() protoreflect.Message {
mi := &file_api_types_types_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Params.ProtoReflect.Descriptor instead.
func (*Params) Descriptor() ([]byte, []int) {
return file_api_types_types_proto_rawDescGZIP(), []int{1}
}
func (x *Params) GetPage() int32 {
if x != nil {
return x.Page
}
return 0
}
func (x *Params) GetLimit() int32 {
if x != nil {
return x.Limit
}
return 0
}
func (x *Params) GetSort() string {
if x != nil {
return x.Sort
}
return ""
}
func (x *Params) GetColumns() []*Column {
if x != nil {
return x.Columns
}
return nil
}
var File_api_types_types_proto protoreflect.FileDescriptor
var file_api_types_types_proto_rawDesc = []byte{
0x0a, 0x15, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x74, 0x79, 0x70, 0x65,
0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x74, 0x79, 0x70, 0x65, 0x73, 0x22, 0x5a,
0x0a, 0x06, 0x43, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65,
0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03,
0x65, 0x78, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x65, 0x78, 0x70, 0x12, 0x14,
0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x18, 0x04, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x6c, 0x6f, 0x67, 0x69, 0x63, 0x22, 0x6f, 0x0a, 0x06, 0x50, 0x61,
0x72, 0x61, 0x6d, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01,
0x28, 0x05, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6c, 0x69, 0x6d, 0x69,
0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x12,
0x0a, 0x04, 0x73, 0x6f, 0x72, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x73, 0x6f,
0x72, 0x74, 0x12, 0x27, 0x0a, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x18, 0x04, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x43, 0x6f, 0x6c, 0x75,
0x6d, 0x6e, 0x52, 0x07, 0x63, 0x6f, 0x6c, 0x75, 0x6d, 0x6e, 0x73, 0x42, 0x2b, 0x5a, 0x29, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x7a, 0x68, 0x75, 0x66, 0x75, 0x79,
0x69, 0x2f, 0x73, 0x70, 0x6f, 0x6e, 0x67, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x74, 0x79, 0x70,
0x65, 0x73, 0x3b, 0x74, 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_api_types_types_proto_rawDescOnce sync.Once
file_api_types_types_proto_rawDescData = file_api_types_types_proto_rawDesc
)
func file_api_types_types_proto_rawDescGZIP() []byte {
file_api_types_types_proto_rawDescOnce.Do(func() {
file_api_types_types_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_types_types_proto_rawDescData)
})
return file_api_types_types_proto_rawDescData
}
var file_api_types_types_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_api_types_types_proto_goTypes = []interface{}{
(*Column)(nil), // 0: types.Column
(*Params)(nil), // 1: types.Params
}
var file_api_types_types_proto_depIdxs = []int32{
0, // 0: types.Params.columns:type_name -> types.Column
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
1, // [1:1] is the sub-list for extension extendee
0, // [0:1] is the sub-list for field type_name
}
func init() { file_api_types_types_proto_init() }
func file_api_types_types_proto_init() {
if File_api_types_types_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_api_types_types_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Column); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_api_types_types_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Params); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_api_types_types_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_api_types_types_proto_goTypes,
DependencyIndexes: file_api_types_types_proto_depIdxs,
MessageInfos: file_api_types_types_proto_msgTypes,
}.Build()
File_api_types_types_proto = out.File
file_api_types_types_proto_rawDesc = nil
file_api_types_types_proto_goTypes = nil
file_api_types_types_proto_depIdxs = nil
}

View File

@ -0,0 +1,280 @@
// Code generated by protoc-gen-validate. DO NOT EDIT.
// source: api/types/types.proto
package types
import (
"bytes"
"errors"
"fmt"
"net"
"net/mail"
"net/url"
"regexp"
"sort"
"strings"
"time"
"unicode/utf8"
"google.golang.org/protobuf/types/known/anypb"
)
// ensure the imports are used
var (
_ = bytes.MinRead
_ = errors.New("")
_ = fmt.Print
_ = utf8.UTFMax
_ = (*regexp.Regexp)(nil)
_ = (*strings.Reader)(nil)
_ = net.IPv4len
_ = time.Duration(0)
_ = (*url.URL)(nil)
_ = (*mail.Address)(nil)
_ = anypb.Any{}
_ = sort.Sort
)
// Validate checks the field values on Column with the rules defined in the
// proto definition for this message. If any rules are violated, the first
// error encountered is returned, or nil if there are no violations.
func (m *Column) Validate() error {
return m.validate(false)
}
// ValidateAll checks the field values on Column with the rules defined in the
// proto definition for this message. If any rules are violated, the result is
// a list of violation errors wrapped in ColumnMultiError, or nil if none found.
func (m *Column) ValidateAll() error {
return m.validate(true)
}
func (m *Column) validate(all bool) error {
if m == nil {
return nil
}
var errors []error
// no validation rules for Name
// no validation rules for Exp
// no validation rules for Value
// no validation rules for Logic
if len(errors) > 0 {
return ColumnMultiError(errors)
}
return nil
}
// ColumnMultiError is an error wrapping multiple validation errors returned by
// Column.ValidateAll() if the designated constraints aren't met.
type ColumnMultiError []error
// Error returns a concatenation of all the error messages it wraps.
func (m ColumnMultiError) Error() string {
var msgs []string
for _, err := range m {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
// AllErrors returns a list of validation violation errors.
func (m ColumnMultiError) AllErrors() []error { return m }
// ColumnValidationError is the validation error returned by Column.Validate if
// the designated constraints aren't met.
type ColumnValidationError struct {
field string
reason string
cause error
key bool
}
// Field function returns field value.
func (e ColumnValidationError) Field() string { return e.field }
// Reason function returns reason value.
func (e ColumnValidationError) Reason() string { return e.reason }
// Cause function returns cause value.
func (e ColumnValidationError) Cause() error { return e.cause }
// Key function returns key value.
func (e ColumnValidationError) Key() bool { return e.key }
// ErrorName returns error name.
func (e ColumnValidationError) ErrorName() string { return "ColumnValidationError" }
// Error satisfies the builtin error interface
func (e ColumnValidationError) Error() string {
cause := ""
if e.cause != nil {
cause = fmt.Sprintf(" | caused by: %v", e.cause)
}
key := ""
if e.key {
key = "key for "
}
return fmt.Sprintf(
"invalid %sColumn.%s: %s%s",
key,
e.field,
e.reason,
cause)
}
var _ error = ColumnValidationError{}
var _ interface {
Field() string
Reason() string
Key() bool
Cause() error
ErrorName() string
} = ColumnValidationError{}
// Validate checks the field values on Params with the rules defined in the
// proto definition for this message. If any rules are violated, the first
// error encountered is returned, or nil if there are no violations.
func (m *Params) Validate() error {
return m.validate(false)
}
// ValidateAll checks the field values on Params with the rules defined in the
// proto definition for this message. If any rules are violated, the result is
// a list of violation errors wrapped in ParamsMultiError, or nil if none found.
func (m *Params) ValidateAll() error {
return m.validate(true)
}
func (m *Params) validate(all bool) error {
if m == nil {
return nil
}
var errors []error
// no validation rules for Page
// no validation rules for Limit
// no validation rules for Sort
for idx, item := range m.GetColumns() {
_, _ = idx, item
if all {
switch v := interface{}(item).(type) {
case interface{ ValidateAll() error }:
if err := v.ValidateAll(); err != nil {
errors = append(errors, ParamsValidationError{
field: fmt.Sprintf("Columns[%v]", idx),
reason: "embedded message failed validation",
cause: err,
})
}
case interface{ Validate() error }:
if err := v.Validate(); err != nil {
errors = append(errors, ParamsValidationError{
field: fmt.Sprintf("Columns[%v]", idx),
reason: "embedded message failed validation",
cause: err,
})
}
}
} else if v, ok := interface{}(item).(interface{ Validate() error }); ok {
if err := v.Validate(); err != nil {
return ParamsValidationError{
field: fmt.Sprintf("Columns[%v]", idx),
reason: "embedded message failed validation",
cause: err,
}
}
}
}
if len(errors) > 0 {
return ParamsMultiError(errors)
}
return nil
}
// ParamsMultiError is an error wrapping multiple validation errors returned by
// Params.ValidateAll() if the designated constraints aren't met.
type ParamsMultiError []error
// Error returns a concatenation of all the error messages it wraps.
func (m ParamsMultiError) Error() string {
var msgs []string
for _, err := range m {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}
// AllErrors returns a list of validation violation errors.
func (m ParamsMultiError) AllErrors() []error { return m }
// ParamsValidationError is the validation error returned by Params.Validate if
// the designated constraints aren't met.
type ParamsValidationError struct {
field string
reason string
cause error
key bool
}
// Field function returns field value.
func (e ParamsValidationError) Field() string { return e.field }
// Reason function returns reason value.
func (e ParamsValidationError) Reason() string { return e.reason }
// Cause function returns cause value.
func (e ParamsValidationError) Cause() error { return e.cause }
// Key function returns key value.
func (e ParamsValidationError) Key() bool { return e.key }
// ErrorName returns error name.
func (e ParamsValidationError) ErrorName() string { return "ParamsValidationError" }
// Error satisfies the builtin error interface
func (e ParamsValidationError) Error() string {
cause := ""
if e.cause != nil {
cause = fmt.Sprintf(" | caused by: %v", e.cause)
}
key := ""
if e.key {
key = "key for "
}
return fmt.Sprintf(
"invalid %sParams.%s: %s%s",
key,
e.field,
e.reason,
cause)
}
var _ error = ParamsValidationError{}
var _ interface {
Field() string
Reason() string
Key() bool
Cause() error
ErrorName() string
} = ParamsValidationError{}

19
api/types/types.proto Normal file
View File

@ -0,0 +1,19 @@
syntax = "proto3";
package types;
option go_package = "github.com/zhufuyi/sponge/api/types;types";
message Column {
string name=1; //
string exp=2; // ==!=>>=<<=like七种类型
string value=3; //
string logic=4; // and&(and)||(or)
}
message Params {
int32 page = 1; // 0
int32 limit = 2; //
string sort = 3; //
repeated Column columns = 4; //
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,97 @@
// todo generate the protobuf code here
// delete the templates code start
syntax = "proto3";
package api.userExample.v1;
import "validate/validate.proto";
import "api/types/types.proto";
option go_package = "github.com/zhufuyi/sponge/api/userExample/v1;v1";
service userExampleService {
rpc Create(CreateUserExampleRequest) returns (CreateUserExampleReply) {}
rpc DeleteByID(DeleteUserExampleByIDRequest) returns (DeleteUserExampleByIDReply) {}
rpc UpdateByID(UpdateUserExampleByIDRequest) returns (UpdateUserExampleByIDReply) {}
rpc GetByID(GetUserExampleByIDRequest) returns (GetUserExampleByIDReply) {}
rpc List(ListUserExampleRequest) returns (ListUserExampleReply) {}
}
enum GenderType {
UNKNOWN = 0;
MALE = 1;
FEMALE = 2;
};
message CreateUserExampleRequest {
string name = 1 [(validate.rules).string.min_len = 2]; //
string email = 2 [(validate.rules).string.email = true]; //
string password = 3 [(validate.rules).string.min_len = 10]; //
string phone=4 [(validate.rules).string = {pattern: "^1[3456789]\\d{9}$"}]; //
string avatar=5 [(validate.rules).string.uri = true]; //
int32 age=6 [(validate.rules).int32 = {gte:0, lte: 120}]; //
GenderType gender=7 [(validate.rules).enum.defined_only = true]; // 1:2:
}
message CreateUserExampleReply {
uint64 id = 1 [(validate.rules).uint64.gte = 1];
}
message DeleteUserExampleByIDRequest {
uint64 id = 1 [(validate.rules).uint64.gte = 1];
}
message DeleteUserExampleByIDReply {
}
message UpdateUserExampleByIDRequest {
uint64 id = 1 [(validate.rules).uint64.gte = 1];
string name = 2; //
string email = 3; //
string password = 4; //
string phone=5; // '+86'
string avatar=6; //
int32 age=7; //
GenderType gender=8; // 1:2:
int32 status=9; //
int64 login_at=10; //
}
message UpdateUserExampleByIDReply {
}
message UserExample {
uint64 id = 1;
string name = 2; //
string email = 3; //
string phone=4; // '+86'
string avatar=5; //
int32 age=6; //
GenderType gender=7; // 1:2:
int32 status=8; //
int64 login_at=9; //
int64 created_at=10; //
int64 updated_at=11; //
}
message GetUserExampleByIDRequest {
uint64 id = 1 [(validate.rules).uint64.gte = 1];
}
message GetUserExampleByIDReply {
UserExample userExample = 1;
}
message ListUserExampleRequest {
types.Params params = 1 [(validate.rules).message.required = true];
}
message ListUserExampleReply {
int64 total =1;
repeated UserExample userExamples = 2;
}
// delete the templates code end

View File

@ -0,0 +1,249 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.20.1
// source: api/userExample/v1/userExample.proto
package v1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.32.0 or later.
const _ = grpc.SupportPackageIsVersion7
// UserExampleServiceClient is the client API for UserExampleService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
type UserExampleServiceClient interface {
Create(ctx context.Context, in *CreateUserExampleRequest, opts ...grpc.CallOption) (*CreateUserExampleReply, error)
DeleteByID(ctx context.Context, in *DeleteUserExampleByIDRequest, opts ...grpc.CallOption) (*DeleteUserExampleByIDReply, error)
UpdateByID(ctx context.Context, in *UpdateUserExampleByIDRequest, opts ...grpc.CallOption) (*UpdateUserExampleByIDReply, error)
GetByID(ctx context.Context, in *GetUserExampleByIDRequest, opts ...grpc.CallOption) (*GetUserExampleByIDReply, error)
List(ctx context.Context, in *ListUserExampleRequest, opts ...grpc.CallOption) (*ListUserExampleReply, error)
}
type userExampleServiceClient struct {
cc grpc.ClientConnInterface
}
func NewUserExampleServiceClient(cc grpc.ClientConnInterface) UserExampleServiceClient {
return &userExampleServiceClient{cc}
}
func (c *userExampleServiceClient) Create(ctx context.Context, in *CreateUserExampleRequest, opts ...grpc.CallOption) (*CreateUserExampleReply, error) {
out := new(CreateUserExampleReply)
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/Create", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userExampleServiceClient) DeleteByID(ctx context.Context, in *DeleteUserExampleByIDRequest, opts ...grpc.CallOption) (*DeleteUserExampleByIDReply, error) {
out := new(DeleteUserExampleByIDReply)
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/DeleteByID", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userExampleServiceClient) UpdateByID(ctx context.Context, in *UpdateUserExampleByIDRequest, opts ...grpc.CallOption) (*UpdateUserExampleByIDReply, error) {
out := new(UpdateUserExampleByIDReply)
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/UpdateByID", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userExampleServiceClient) GetByID(ctx context.Context, in *GetUserExampleByIDRequest, opts ...grpc.CallOption) (*GetUserExampleByIDReply, error) {
out := new(GetUserExampleByIDReply)
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/GetByID", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *userExampleServiceClient) List(ctx context.Context, in *ListUserExampleRequest, opts ...grpc.CallOption) (*ListUserExampleReply, error) {
out := new(ListUserExampleReply)
err := c.cc.Invoke(ctx, "/api.userExample.v1.userExampleService/List", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
// UserExampleServiceServer is the server API for UserExampleService service.
// All implementations must embed UnimplementedUserExampleServiceServer
// for forward compatibility
type UserExampleServiceServer interface {
Create(context.Context, *CreateUserExampleRequest) (*CreateUserExampleReply, error)
DeleteByID(context.Context, *DeleteUserExampleByIDRequest) (*DeleteUserExampleByIDReply, error)
UpdateByID(context.Context, *UpdateUserExampleByIDRequest) (*UpdateUserExampleByIDReply, error)
GetByID(context.Context, *GetUserExampleByIDRequest) (*GetUserExampleByIDReply, error)
List(context.Context, *ListUserExampleRequest) (*ListUserExampleReply, error)
mustEmbedUnimplementedUserExampleServiceServer()
}
// UnimplementedUserExampleServiceServer must be embedded to have forward compatible implementations.
type UnimplementedUserExampleServiceServer struct {
}
func (UnimplementedUserExampleServiceServer) Create(context.Context, *CreateUserExampleRequest) (*CreateUserExampleReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method Create not implemented")
}
func (UnimplementedUserExampleServiceServer) DeleteByID(context.Context, *DeleteUserExampleByIDRequest) (*DeleteUserExampleByIDReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteByID not implemented")
}
func (UnimplementedUserExampleServiceServer) UpdateByID(context.Context, *UpdateUserExampleByIDRequest) (*UpdateUserExampleByIDReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateByID not implemented")
}
func (UnimplementedUserExampleServiceServer) GetByID(context.Context, *GetUserExampleByIDRequest) (*GetUserExampleByIDReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetByID not implemented")
}
func (UnimplementedUserExampleServiceServer) List(context.Context, *ListUserExampleRequest) (*ListUserExampleReply, error) {
return nil, status.Errorf(codes.Unimplemented, "method List not implemented")
}
func (UnimplementedUserExampleServiceServer) mustEmbedUnimplementedUserExampleServiceServer() {}
// UnsafeUserExampleServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to UserExampleServiceServer will
// result in compilation errors.
type UnsafeUserExampleServiceServer interface {
mustEmbedUnimplementedUserExampleServiceServer()
}
func RegisterUserExampleServiceServer(s grpc.ServiceRegistrar, srv UserExampleServiceServer) {
s.RegisterService(&UserExampleService_ServiceDesc, srv)
}
func _UserExampleService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateUserExampleRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserExampleServiceServer).Create(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.userExample.v1.userExampleService/Create",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserExampleServiceServer).Create(ctx, req.(*CreateUserExampleRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserExampleService_DeleteByID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteUserExampleByIDRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserExampleServiceServer).DeleteByID(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.userExample.v1.userExampleService/DeleteByID",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserExampleServiceServer).DeleteByID(ctx, req.(*DeleteUserExampleByIDRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserExampleService_UpdateByID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateUserExampleByIDRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserExampleServiceServer).UpdateByID(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.userExample.v1.userExampleService/UpdateByID",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserExampleServiceServer).UpdateByID(ctx, req.(*UpdateUserExampleByIDRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserExampleService_GetByID_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetUserExampleByIDRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserExampleServiceServer).GetByID(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.userExample.v1.userExampleService/GetByID",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserExampleServiceServer).GetByID(ctx, req.(*GetUserExampleByIDRequest))
}
return interceptor(ctx, in, info, handler)
}
func _UserExampleService_List_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListUserExampleRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserExampleServiceServer).List(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/api.userExample.v1.userExampleService/List",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserExampleServiceServer).List(ctx, req.(*ListUserExampleRequest))
}
return interceptor(ctx, in, info, handler)
}
// UserExampleService_ServiceDesc is the grpc.ServiceDesc for UserExampleService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var UserExampleService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "api.userExample.v1.userExampleService",
HandlerType: (*UserExampleServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "Create",
Handler: _UserExampleService_Create_Handler,
},
{
MethodName: "DeleteByID",
Handler: _UserExampleService_DeleteByID_Handler,
},
{
MethodName: "UpdateByID",
Handler: _UserExampleService_UpdateByID_Handler,
},
{
MethodName: "GetByID",
Handler: _UserExampleService_GetByID_Handler,
},
{
MethodName: "List",
Handler: _UserExampleService_List_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "api/userExample/v1/userExample.proto",
}

197
cmd/sponge/main.go Normal file
View File

@ -0,0 +1,197 @@
package main
import (
"context"
"flag"
"fmt"
"strconv"
"time"
"github.com/zhufuyi/sponge/config"
"github.com/zhufuyi/sponge/internal/model"
"github.com/zhufuyi/sponge/internal/server"
"github.com/zhufuyi/sponge/pkg/app"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/zhufuyi/sponge/pkg/registry"
"github.com/zhufuyi/sponge/pkg/registry/etcd"
"github.com/zhufuyi/sponge/pkg/tracer"
clientv3 "go.etcd.io/etcd/client/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
var (
version string
configFile string
)
// @title sponge api docs
// @description http server api docs
// @schemes http https
// @version v0.0.0
// @host localhost:8080
func main() {
inits := registerInits()
servers := registerServers()
closes := registerCloses(servers)
s := app.New(inits, servers, closes)
s.Run()
}
// -------------------------------- 注册app初始化 ---------------------------------
func registerInits() []app.Init {
// 初始化配置文件,必须优先执行,后面的初始化需要依赖配置
func() {
flag.StringVar(&configFile, "c", "", "配置文件")
flag.StringVar(&version, "version", "", "服务版本号")
flag.Parse()
if configFile == "" {
configFile = config.Path("conf.yml") // 默认配置文件config/conf.yml
}
err := config.Init(configFile)
if err != nil {
panic("init config error: " + err.Error())
}
if version != "" {
config.Get().App.Version = version
}
//config.Show()
}()
// 执行初始化日志
func() {
_, err := logger.Init(
logger.WithLevel(config.Get().Logger.Level),
logger.WithFormat(config.Get().Logger.Format),
logger.WithSave(config.Get().Logger.IsSave,
logger.WithFileName(config.Get().Logger.LogFileConfig.Filename),
logger.WithFileMaxSize(config.Get().Logger.LogFileConfig.MaxSize),
logger.WithFileMaxBackups(config.Get().Logger.LogFileConfig.MaxBackups),
logger.WithFileMaxAge(config.Get().Logger.LogFileConfig.MaxAge),
logger.WithFileIsCompression(config.Get().Logger.LogFileConfig.IsCompression),
),
)
if err != nil {
panic("init logger error: " + err.Error())
}
}()
var inits []app.Init
// 初始化数据库
inits = append(inits, func() {
model.InitMysql()
model.InitRedis()
})
if config.Get().App.EnableTracing { // 根据配置是否开启链路跟踪
inits = append(inits, func() {
// 初始化链路跟踪
exporter, err := tracer.NewJaegerAgentExporter(config.Get().Jaeger.AgentHost, config.Get().Jaeger.AgentPort)
if err != nil {
panic("init trace error:" + err.Error())
}
resource := tracer.NewResource(
tracer.WithServiceName(config.Get().App.Name),
tracer.WithEnvironment(config.Get().App.Env),
tracer.WithServiceVersion(config.Get().App.Version),
)
tracer.Init(exporter, resource, config.Get().Jaeger.SamplingRate) // 如果SamplingRate=0.5表示只采样50%
})
}
return inits
}
// -------------------------------- 注册app服务 ---------------------------------
// todo generate the code to register http and grpc services here
// delete the templates code start
func registerServers() []app.IServer {
var servers []app.IServer
// 创建http服务
httpAddr := ":" + strconv.Itoa(config.Get().HTTP.Port)
httpServer := server.NewHTTPServer(httpAddr,
server.WithHTTPReadTimeout(time.Second*time.Duration(config.Get().HTTP.ReadTimeout)),
server.WithHTTPWriteTimeout(time.Second*time.Duration(config.Get().HTTP.WriteTimeout)),
server.WithHTTPIsProd(config.Get().App.Env == "prod"),
)
servers = append(servers, httpServer)
// 创建grpc服务
grpcAddr := ":" + strconv.Itoa(config.Get().Grpc.Port)
grpcServer := server.NewGRPCServer(grpcAddr, grpcOptions()...)
servers = append(servers, grpcServer)
return servers
}
func grpcOptions() []server.GRPCOption {
var opts []server.GRPCOption
if config.Get().App.EnableRegistryDiscovery {
iRegistry, instance := getETCDRegistry(
config.Get().Etcd.Addrs,
config.Get().App.Name,
[]string{fmt.Sprintf("grpc://%s:%d", config.Get().App.Host, config.Get().Grpc.Port)},
)
opts = append(opts, server.WithRegistry(iRegistry, instance))
}
return opts
}
func getETCDRegistry(etcdEndpoints []string, instanceName string, instanceEndpoints []string) (registry.Registry, *registry.ServiceInstance) {
serviceInstance := registry.NewServiceInstance(instanceName, instanceEndpoints)
cli, err := clientv3.New(clientv3.Config{
Endpoints: etcdEndpoints,
DialTimeout: 5 * time.Second,
DialOptions: []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
},
})
if err != nil {
panic(err)
}
iRegistry := etcd.New(cli)
return iRegistry, serviceInstance
}
// delete the templates code end
// -------------------------- 注册app需要释放的资源 -------------------------------------------
func registerCloses(servers []app.IServer) []app.Close {
var closes []app.Close
// 关闭服务
for _, s := range servers {
closes = append(closes, s.Stop)
}
// 关闭mysql
closes = append(closes, func() error {
return model.CloseMysql()
})
// 关闭redis
closes = append(closes, func() error {
return model.CloseRedis()
})
// 关闭trace
if config.Get().App.EnableTracing {
closes = append(closes, func() error {
ctx, _ := context.WithTimeout(context.Background(), 2*time.Second) //nolint
return tracer.Close(ctx)
})
}
return closes
}

123
config/conf.go Normal file
View File

@ -0,0 +1,123 @@
// nolint
// code generated from config file.
package config
import "github.com/zhufuyi/sponge/pkg/conf"
type Config = GenerateName
var config *Config
// Init parsing configuration files to struct, including yaml, toml, json, etc.
func Init(configFile string) error {
config = &Config{}
return conf.Parse(configFile, config)
}
func Show() {
conf.Show(config)
}
func Get() *Config {
if config == nil {
panic("config is nil")
}
return config
}
type GenerateName struct {
App App `yaml:"app" json:"app"`
Etcd Etcd `yaml:"etcd" json:"etcd"`
Grpc Grpc `yaml:"grpc" json:"grpc"`
HTTP HTTP `yaml:"http" json:"http"`
Jaeger Jaeger `yaml:"jaeger" json:"jaeger"`
Logger Logger `yaml:"logger" json:"logger"`
Metrics Metrics `yaml:"metrics" json:"metrics"`
Mysql Mysql `yaml:"mysql" json:"mysql"`
RateLimiter RateLimiter `yaml:"rateLimiter" json:"rateLimiter"`
Redis Redis `yaml:"redis" json:"redis"`
}
type Redis struct {
Addr string `yaml:"addr" json:"addr"`
DB int `yaml:"dB" json:"dB"`
DialTimeout int `yaml:"dialTimeout" json:"dialTimeout"`
Dsn string `yaml:"dsn" json:"dsn"`
MinIdleConn int `yaml:"minIdleConn" json:"minIdleConn"`
Password string `yaml:"password" json:"password"`
PoolSize int `yaml:"poolSize" json:"poolSize"`
PoolTimeout int `yaml:"poolTimeout" json:"poolTimeout"`
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
}
type Etcd struct {
Addrs []string `yaml:"addrs" json:"addrs"`
}
type Jaeger struct {
AgentHost string `yaml:"agentHost" json:"agentHost"`
AgentPort string `yaml:"agentPort" json:"agentPort"`
SamplingRate float64 `yaml:"samplingRate" json:"samplingRate"`
}
type Mysql struct {
ConnMaxLifetime int `yaml:"connMaxLifetime" json:"connMaxLifetime"`
Dsn string `yaml:"dsn" json:"dsn"`
EnableLog bool `yaml:"enableLog" json:"enableLog"`
MaxIdleConns int `yaml:"maxIdleConns" json:"maxIdleConns"`
MaxOpenConns int `yaml:"maxOpenConns" json:"maxOpenConns"`
SlowThreshold int `yaml:"slowThreshold" json:"slowThreshold"`
}
type RateLimiter struct {
Dimension string `yaml:"dimension" json:"dimension"`
MaxLimit int `yaml:"maxLimit" json:"maxLimit"`
QPSLimit int `yaml:"qpsLimit" json:"qpsLimit"`
}
type App struct {
EnableRegistryDiscovery bool `yaml:"enableRegistryDiscovery" json:"enableRegistryDiscovery"`
EnableLimit bool `yaml:"enableLimit" json:"enableLimit"`
EnableMetrics bool `yaml:"enableMetrics" json:"enableMetrics"`
EnableProfile bool `yaml:"enableProfile" json:"enableProfile"`
EnableTracing bool `yaml:"enableTracing" json:"enableTracing"`
Env string `yaml:"env" json:"env"`
Host string `yaml:"hostIP" json:"hostIP"`
Name string `yaml:"name" json:"name"`
Version string `yaml:"version" json:"version"`
}
type LogFileConfig struct {
Filename string `yaml:"filename" json:"filename"`
IsCompression bool `yaml:"isCompression" json:"isCompression"`
MaxAge int `yaml:"maxAge" json:"maxAge"`
MaxBackups int `yaml:"maxBackups" json:"maxBackups"`
MaxSize int `yaml:"maxSize" json:"maxSize"`
}
type Logger struct {
Format string `yaml:"format" json:"format"`
IsSave bool `yaml:"isSave" json:"isSave"`
Level string `yaml:"level" json:"level"`
LogFileConfig LogFileConfig `yaml:"logFileConfig" json:"logFileConfig"`
}
type HTTP struct {
Port int `yaml:"port" json:"port"`
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
ServiceName string `yaml:"serviceName" json:"serviceName"`
}
type Grpc struct {
Port int `yaml:"port" json:"port"`
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
ServiceName string `yaml:"serviceName" json:"serviceName"`
}
type Metrics struct {
Port int `yaml:"port" json:"port"`
}

92
config/conf.yml Normal file
View File

@ -0,0 +1,92 @@
# 使用工具自动生成go struct
# goctl covert yaml --file=conf.yml --tags=json
# app 设置
app:
name: "userExample" # 服务名称
env: "dev" # 运行环境dev:开发环境prod:生产环境pre:预生产环境
version: "v0.0.0" # 版本
host: "127.0.0.1" # 主机ip或域名
enableProfile: false # 是否开启性能分析功能true:开启false:关闭
enableMetrics: true # 是否开启指标采集true:开启false:关闭
enableLimit: false # 是否开启限流true:开启false:关闭
enableTracing: false # 是否开启链路跟踪true:开启false:关闭
enableRegistryDiscovery: true # 是否开启注册与发现true:开启false:关闭
# http 设置
http:
port: 8080 # 监听端口
readTimeout: 3 # 读超时,单位(秒)
writeTimeout: 90 # 写超时,单位(秒)如果enableProfile为true需要大于60spprof做profiling的默认值是60s
# grpc 服务设置
grpc:
port: 9090 # 监听端口
readTimeout: 3 # 读超时,单位(秒)
writeTimeout: 3 # 写超时,单位(秒)
# logger 设置
logger:
level: "info" # 输出日志级别 debug, info, warn, error默认是debug
format: "console" # 输出格式console或json默认是console
isSave: false # false:输出到终端true:输出到文件默认是false
logFileConfig: # isSave=true时有效
filename: "out.log" # 文件名称默认值out.log
maxSize: 20 # 最大文件大小(MB)默认值10MB
maxBackups: 50 # 保留旧文件的最大个数默认值100个
maxAge: 15 # 保留旧文件的最大天数默认值30天
isCompression: true # 是否压缩/归档旧文件默认值false
# mysql 设置
mysql:
# dsn格式<user>:<pass>@(127.0.0.1:3306)/<db>?[k=v& ......]
dsn: "root:123456@(192.168.3.37:3306)/account?parseTime=true&loc=Local&charset=utf8,utf8mb4"
enableLog: true # 是否开启打印所有日志
slowThreshold: 0 # 如果大于0只打印时间大于阈值的日志优先级比enableLog高单位(毫秒)
maxIdleConns: 3 #设置空闲连接池中连接的最大数量
maxOpenConns: 50 # 设置打开数据库连接的最大数量
connMaxLifetime: 30 # 设置了连接可复用的最大时间,单位(分钟)
# redis 设置
redis:
# dsn只适合redis6版本以上默认用户为defaulturl格式 [user]:<pass>@]127.0.0.1:6379/[db]
dsn: "default:123456@192.168.3.37:6379"
# 适合各个版本redis
addr: 127.0.0.1:6379
password: "123456"
dB: 0
minIdleConn: 20
dialTimeout: 30 # 链接超时,单位(秒)
readTimeout: 500 # 读超时,单位(毫秒)
writeTimeout: 500 # 写超时,单位(毫秒)
poolSize: 100
poolTimeout: 200 # 连接池超时,单位(秒)
# jaeger配置
jaeger:
agentHost: "192.168.3.37"
agentPort: "6831"
samplingRate: 1.0 # 采样率0~1之间0表示禁止采样大于等于1表示采样所有链路
# limit配置
rateLimiter:
dimension: "path" # 限流维度支持path和ip两种默认是path
qpsLimit: 1000 # 持续每秒允许成功请求数默认是500
maxLimit: 2000 # 瞬时最大允许峰值默认是1000通常大于qpsLimit
# metrics配置
metrics:
port: 9082
# etcd配置
etcd:
addrs: ["192.168.3.37:2379"]

25
config/location.go Normal file
View File

@ -0,0 +1,25 @@
package config
import (
"path/filepath"
"runtime"
)
// 用来定位目录
var basePath string
// nolint
func init() {
_, currentFile, _, _ := runtime.Caller(0)
basePath = filepath.Dir(currentFile)
}
// Path 返回绝对路径
func Path(rel string) string {
if filepath.IsAbs(rel) {
return rel
}
return filepath.Join(basePath, rel)
}

4
doc.go Normal file
View File

@ -0,0 +1,4 @@
/*
sponge 是一个微服务框架支持http和grpc及服务治理.
*/
package sponge

392
docs/docs.go Normal file
View File

@ -0,0 +1,392 @@
// Package docs GENERATED BY SWAG; DO NOT EDIT
// This file was generated by swaggo/swag
package docs
import "github.com/swaggo/swag"
const docTemplate = `{
"schemes": {{ marshal .Schemes }},
"swagger": "2.0",
"info": {
"description": "{{escape .Description}}",
"title": "{{.Title}}",
"contact": {},
"version": "{{.Version}}"
},
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/api/v1/userExample": {
"post": {
"description": "提交信息创建userExample",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "创建userExample",
"parameters": [
{
"description": "userExample信息",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.CreateUserExampleRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
}
},
"/api/v1/userExample/{id}": {
"get": {
"description": "根据id获取userExample详情",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "获取userExample详情",
"parameters": [
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
},
"put": {
"description": "根据id更新userExample信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "更新userExample信息",
"parameters": [
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
},
{
"description": "userExample信息",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdateUserExampleByIDRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
},
"delete": {
"description": "根据id删除userExample",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "删除userExample",
"parameters": [
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
}
},
"/api/v1/userExamples": {
"post": {
"description": "使用post请求获取userExample列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "获取userExample列表",
"parameters": [
{
"description": "查询条件",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.Params"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
}
},
"/health": {
"get": {
"description": "check health",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "check health",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlerfunc.checkHealthResponse"
}
}
}
}
},
"/ping": {
"get": {
"description": "ping",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "ping",
"responses": {}
}
}
},
"definitions": {
"handler.Column": {
"type": "object",
"properties": {
"exp": {
"description": "表达式,值为空时默认为=,有=、!=、\u003e、\u003e=、\u003c、\u003c=、like七种类型",
"type": "string"
},
"logic": {
"description": "逻辑类型值为空时默认为and有\u0026(and)、||(or)两种类型",
"type": "string"
},
"name": {
"description": "列名",
"type": "string"
},
"value": {
"description": "列值"
}
}
},
"handler.CreateUserExampleRequest": {
"type": "object",
"properties": {
"age": {
"description": "年龄",
"type": "integer"
},
"avatar": {
"description": "头像",
"type": "string",
"minLength": 5
},
"email": {
"description": "邮件",
"type": "string"
},
"gender": {
"description": "性别1:男2:女",
"type": "integer",
"maximum": 2,
"minimum": 0
},
"name": {
"description": "名称",
"type": "string",
"minLength": 2
},
"password": {
"description": "密码",
"type": "string"
},
"phone": {
"description": "手机号码,必须在前加'+86'",
"type": "string"
}
}
},
"handler.Params": {
"type": "object",
"properties": {
"columns": {
"description": "列查询条件",
"type": "array",
"items": {
"$ref": "#/definitions/handler.Column"
}
},
"page": {
"description": "页码",
"type": "integer",
"minimum": 0
},
"size": {
"description": "每页行数",
"type": "integer"
},
"sort": {
"description": "排序字段,默认值为-id字段前面有-号表示倒序,否则升序,多个字段用逗号分隔",
"type": "string"
}
}
},
"handler.Result": {
"type": "object",
"properties": {
"code": {
"description": "返回码",
"type": "integer"
},
"data": {
"description": "返回数据"
},
"msg": {
"description": "返回信息说明",
"type": "string"
}
}
},
"handler.UpdateUserExampleByIDRequest": {
"type": "object",
"properties": {
"age": {
"description": "年龄",
"type": "integer"
},
"avatar": {
"description": "头像",
"type": "string"
},
"email": {
"description": "邮件",
"type": "string"
},
"gender": {
"description": "性别1:男2:女",
"type": "integer"
},
"id": {
"description": "id",
"type": "integer"
},
"name": {
"description": "名称",
"type": "string"
},
"password": {
"description": "密码",
"type": "string"
},
"phone": {
"description": "手机号码,必须在前加'+86'",
"type": "string"
}
}
},
"handlerfunc.checkHealthResponse": {
"type": "object",
"properties": {
"hostname": {
"type": "string"
},
"status": {
"type": "string"
}
}
}
}
}`
// SwaggerInfo holds exported Swagger Info so clients can modify it
var SwaggerInfo = &swag.Spec{
Version: "v0.0.0",
Host: "localhost:8080",
BasePath: "",
Schemes: []string{"http", "https"},
Title: "sponge api docs",
Description: "http server api docs",
InfoInstanceName: "swagger",
SwaggerTemplate: docTemplate,
}
func init() {
swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo)
}

1291
docs/proto.html Normal file

File diff suppressed because it is too large Load Diff

372
docs/swagger.json Normal file
View File

@ -0,0 +1,372 @@
{
"schemes": [
"http",
"https"
],
"swagger": "2.0",
"info": {
"description": "http server api docs",
"title": "sponge api docs",
"contact": {},
"version": "v0.0.0"
},
"host": "localhost:8080",
"paths": {
"/api/v1/userExample": {
"post": {
"description": "提交信息创建userExample",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "创建userExample",
"parameters": [
{
"description": "userExample信息",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.CreateUserExampleRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
}
},
"/api/v1/userExample/{id}": {
"get": {
"description": "根据id获取userExample详情",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "获取userExample详情",
"parameters": [
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
},
"put": {
"description": "根据id更新userExample信息",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "更新userExample信息",
"parameters": [
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
},
{
"description": "userExample信息",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.UpdateUserExampleByIDRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
},
"delete": {
"description": "根据id删除userExample",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "删除userExample",
"parameters": [
{
"type": "string",
"description": "id",
"name": "id",
"in": "path",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
}
},
"/api/v1/userExamples": {
"post": {
"description": "使用post请求获取userExample列表",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"userExample"
],
"summary": "获取userExample列表",
"parameters": [
{
"description": "查询条件",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/handler.Params"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.Result"
}
}
}
}
},
"/health": {
"get": {
"description": "check health",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "check health",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handlerfunc.checkHealthResponse"
}
}
}
}
},
"/ping": {
"get": {
"description": "ping",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"system"
],
"summary": "ping",
"responses": {}
}
}
},
"definitions": {
"handler.Column": {
"type": "object",
"properties": {
"exp": {
"description": "表达式,值为空时默认为=,有=、!=、\u003e、\u003e=、\u003c、\u003c=、like七种类型",
"type": "string"
},
"logic": {
"description": "逻辑类型值为空时默认为and有\u0026(and)、||(or)两种类型",
"type": "string"
},
"name": {
"description": "列名",
"type": "string"
},
"value": {
"description": "列值"
}
}
},
"handler.CreateUserExampleRequest": {
"type": "object",
"properties": {
"age": {
"description": "年龄",
"type": "integer"
},
"avatar": {
"description": "头像",
"type": "string",
"minLength": 5
},
"email": {
"description": "邮件",
"type": "string"
},
"gender": {
"description": "性别1:男2:女",
"type": "integer",
"maximum": 2,
"minimum": 0
},
"name": {
"description": "名称",
"type": "string",
"minLength": 2
},
"password": {
"description": "密码",
"type": "string"
},
"phone": {
"description": "手机号码,必须在前加'+86'",
"type": "string"
}
}
},
"handler.Params": {
"type": "object",
"properties": {
"columns": {
"description": "列查询条件",
"type": "array",
"items": {
"$ref": "#/definitions/handler.Column"
}
},
"page": {
"description": "页码",
"type": "integer",
"minimum": 0
},
"size": {
"description": "每页行数",
"type": "integer"
},
"sort": {
"description": "排序字段,默认值为-id字段前面有-号表示倒序,否则升序,多个字段用逗号分隔",
"type": "string"
}
}
},
"handler.Result": {
"type": "object",
"properties": {
"code": {
"description": "返回码",
"type": "integer"
},
"data": {
"description": "返回数据"
},
"msg": {
"description": "返回信息说明",
"type": "string"
}
}
},
"handler.UpdateUserExampleByIDRequest": {
"type": "object",
"properties": {
"age": {
"description": "年龄",
"type": "integer"
},
"avatar": {
"description": "头像",
"type": "string"
},
"email": {
"description": "邮件",
"type": "string"
},
"gender": {
"description": "性别1:男2:女",
"type": "integer"
},
"id": {
"description": "id",
"type": "integer"
},
"name": {
"description": "名称",
"type": "string"
},
"password": {
"description": "密码",
"type": "string"
},
"phone": {
"description": "手机号码,必须在前加'+86'",
"type": "string"
}
}
},
"handlerfunc.checkHealthResponse": {
"type": "object",
"properties": {
"hostname": {
"type": "string"
},
"status": {
"type": "string"
}
}
}
}
}

254
docs/swagger.yaml Normal file
View File

@ -0,0 +1,254 @@
definitions:
handler.Column:
properties:
exp:
description: 表达式,值为空时默认为=,有=、!=、>、>=、<、<=、like七种类型
type: string
logic:
description: 逻辑类型值为空时默认为and有&(and)、||(or)两种类型
type: string
name:
description: 列名
type: string
value:
description: 列值
type: object
handler.CreateUserExampleRequest:
properties:
age:
description: 年龄
type: integer
avatar:
description: 头像
minLength: 5
type: string
email:
description: 邮件
type: string
gender:
description: 性别1:男2:女
maximum: 2
minimum: 0
type: integer
name:
description: 名称
minLength: 2
type: string
password:
description: 密码
type: string
phone:
description: 手机号码,必须在前加'+86'
type: string
type: object
handler.Params:
properties:
columns:
description: 列查询条件
items:
$ref: '#/definitions/handler.Column'
type: array
page:
description: 页码
minimum: 0
type: integer
size:
description: 每页行数
type: integer
sort:
description: 排序字段,默认值为-id字段前面有-号表示倒序,否则升序,多个字段用逗号分隔
type: string
type: object
handler.Result:
properties:
code:
description: 返回码
type: integer
data:
description: 返回数据
msg:
description: 返回信息说明
type: string
type: object
handler.UpdateUserExampleByIDRequest:
properties:
age:
description: 年龄
type: integer
avatar:
description: 头像
type: string
email:
description: 邮件
type: string
gender:
description: 性别1:男2:女
type: integer
id:
description: id
type: integer
name:
description: 名称
type: string
password:
description: 密码
type: string
phone:
description: 手机号码,必须在前加'+86'
type: string
type: object
handlerfunc.checkHealthResponse:
properties:
hostname:
type: string
status:
type: string
type: object
host: localhost:8080
info:
contact: {}
description: http server api docs
title: sponge api docs
version: v0.0.0
paths:
/api/v1/userExample:
post:
consumes:
- application/json
description: 提交信息创建userExample
parameters:
- description: userExample信息
in: body
name: data
required: true
schema:
$ref: '#/definitions/handler.CreateUserExampleRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.Result'
summary: 创建userExample
tags:
- userExample
/api/v1/userExample/{id}:
delete:
consumes:
- application/json
description: 根据id删除userExample
parameters:
- description: id
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.Result'
summary: 删除userExample
tags:
- userExample
get:
consumes:
- application/json
description: 根据id获取userExample详情
parameters:
- description: id
in: path
name: id
required: true
type: string
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.Result'
summary: 获取userExample详情
tags:
- userExample
put:
consumes:
- application/json
description: 根据id更新userExample信息
parameters:
- description: id
in: path
name: id
required: true
type: string
- description: userExample信息
in: body
name: data
required: true
schema:
$ref: '#/definitions/handler.UpdateUserExampleByIDRequest'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.Result'
summary: 更新userExample信息
tags:
- userExample
/api/v1/userExamples:
post:
consumes:
- application/json
description: 使用post请求获取userExample列表
parameters:
- description: 查询条件
in: body
name: data
required: true
schema:
$ref: '#/definitions/handler.Params'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.Result'
summary: 获取userExample列表
tags:
- userExample
/health:
get:
consumes:
- application/json
description: check health
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handlerfunc.checkHealthResponse'
summary: check health
tags:
- system
/ping:
get:
consumes:
- application/json
description: ping
produces:
- application/json
responses: {}
summary: ping
tags:
- system
schemes:
- http
- https
swagger: "2.0"

164
go.mod Normal file
View File

@ -0,0 +1,164 @@
module github.com/zhufuyi/sponge
go 1.19
require (
github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5
github.com/alicebob/miniredis/v2 v2.23.0
github.com/bojand/ghz v0.110.0
github.com/dgraph-io/ristretto v0.1.0
github.com/envoyproxy/protoc-gen-validate v0.6.2
github.com/fsnotify/fsnotify v1.5.4
github.com/gin-contrib/cors v1.3.1
github.com/gin-contrib/pprof v1.4.0
github.com/gin-gonic/gin v1.8.1
github.com/go-playground/validator/v10 v10.11.0
github.com/go-redis/redis/extra/redisotel v0.3.0
github.com/go-redis/redis/v8 v8.11.5
github.com/gogo/protobuf v1.3.2
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2
github.com/golang/snappy v0.0.3
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
github.com/gunerhuseyin/goprometheus v0.0.2
github.com/hashicorp/consul/api v1.12.0
github.com/huandu/xstrings v1.3.1
github.com/jinzhu/copier v0.3.5
github.com/nacos-group/nacos-sdk-go v1.1.2
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/prometheus/client_golang v1.13.0
github.com/reugn/equalizer v0.0.0-20210216135016-a959c509d7ad
github.com/spf13/cast v1.5.0
github.com/spf13/viper v1.12.0
github.com/stretchr/testify v1.8.0
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a
github.com/swaggo/gin-swagger v1.5.2
github.com/swaggo/swag v1.8.1
github.com/uptrace/opentelemetry-go-extra/otelgorm v0.1.15
github.com/vmihailenco/msgpack v4.0.4+incompatible
go.etcd.io/etcd/client/v3 v3.5.4
go.opentelemetry.io/contrib v1.9.0
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.34.0
go.opentelemetry.io/otel v1.9.0
go.opentelemetry.io/otel/exporters/jaeger v1.9.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.9.0
go.opentelemetry.io/otel/sdk v1.9.0
go.opentelemetry.io/otel/trace v1.9.0
go.uber.org/zap v1.21.0
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
google.golang.org/grpc v1.48.0
google.golang.org/protobuf v1.28.1
gorm.io/driver/mysql v1.3.5
gorm.io/gorm v1.23.8
)
require (
cloud.google.com/go/compute v1.6.1 // indirect
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/DataDog/datadog-go v4.8.3+incompatible // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.2 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
github.com/armon/go-metrics v0.3.10 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/cactus/go-statsd-client/statsd v0.0.0-00010101000000-000000000000 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
github.com/coreos/go-semver v0.3.0 // indirect
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-redis/redis/extra/rediscmd v0.2.0 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-hclog v1.2.0 // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/serf v0.9.7 // indirect
github.com/imdario/mergo v0.3.11 // indirect
github.com/jhump/protoreflect v1.9.0 // indirect
github.com/jinzhu/configor v1.1.1 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/reflectwalk v1.0.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/smartystreets/goconvey v1.7.2 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.3.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect
github.com/uptrace/opentelemetry-go-extra/otelsql v0.1.15 // indirect
github.com/yuin/gopher-lua v0.0.0-20210529063254-f4c35e4016d9 // indirect
go.etcd.io/etcd/api/v3 v3.5.4 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect
go.opentelemetry.io/otel/metric v0.31.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.11-0.20220316014157-77aa08bb151a // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
gopkg.in/ini.v1 v1.66.4 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.9.0
replace github.com/cactus/go-statsd-client/statsd => github.com/cactus/go-statsd-client/statsd v0.0.0-20200423205355-cb0885a1018c

1232
go.sum Normal file

File diff suppressed because it is too large Load Diff

138
internal/cache/userExample.go vendored Normal file
View File

@ -0,0 +1,138 @@
package cache
import (
"context"
"fmt"
"time"
"github.com/zhufuyi/sponge/internal/model"
"github.com/zhufuyi/sponge/pkg/cache"
"github.com/zhufuyi/sponge/pkg/encoding"
"github.com/go-redis/redis/v8"
"github.com/spf13/cast"
)
const (
// PrefixUserExampleCacheKey cache prefix
PrefixUserExampleCacheKey = "userExample:%d"
)
var _ UserExampleCache = (*userExampleCache)(nil)
// UserExampleCache cache interface
type UserExampleCache interface {
Set(ctx context.Context, id uint64, data *model.UserExample, duration time.Duration) error
Get(ctx context.Context, id uint64) (ret *model.UserExample, err error)
MultiGet(ctx context.Context, ids []uint64) (map[string]*model.UserExample, error)
MultiSet(ctx context.Context, data []*model.UserExample, duration time.Duration) error
Del(ctx context.Context, id uint64) error
SetCacheWithNotFound(ctx context.Context, id uint64) error
}
// userExampleCache define a cache struct
type userExampleCache struct {
cache cache.Cache
}
// NewUserExampleCache new a cache
func NewUserExampleCache(rdb *redis.Client) UserExampleCache {
jsonEncoding := encoding.JSONEncoding{}
cachePrefix := ""
return &userExampleCache{
cache: cache.NewRedisCache(rdb, cachePrefix, jsonEncoding, func() interface{} {
return &model.UserExample{}
}),
}
}
// GetUserExampleCacheKey 设置缓存
func (c *userExampleCache) GetUserExampleCacheKey(id uint64) string {
return fmt.Sprintf(PrefixUserExampleCacheKey, id)
}
// Set write to cache
func (c *userExampleCache) Set(ctx context.Context, id uint64, data *model.UserExample, duration time.Duration) error {
if data == nil || id == 0 {
return nil
}
cacheKey := c.GetUserExampleCacheKey(id)
err := c.cache.Set(ctx, cacheKey, data, duration)
if err != nil {
return err
}
return nil
}
// Get 获取cache
func (c *userExampleCache) Get(ctx context.Context, id uint64) (*model.UserExample, error) {
var data *model.UserExample
cacheKey := c.GetUserExampleCacheKey(id)
err := c.cache.Get(ctx, cacheKey, &data)
if err != nil {
return nil, err
}
return data, nil
}
// MultiSet 批量设置cache
func (c *userExampleCache) MultiSet(ctx context.Context, data []*model.UserExample, duration time.Duration) error {
valMap := make(map[string]interface{})
for _, v := range data {
cacheKey := c.GetUserExampleCacheKey(v.ID)
valMap[cacheKey] = v
}
err := c.cache.MultiSet(ctx, valMap, duration)
if err != nil {
return err
}
return nil
}
// MultiGet 批量获取cache返回map中的key是id值
func (c *userExampleCache) MultiGet(ctx context.Context, ids []uint64) (map[string]*model.UserExample, error) {
var keys []string
for _, v := range ids {
cacheKey := c.GetUserExampleCacheKey(v)
keys = append(keys, cacheKey)
}
// NOTE: 需要在这里make实例化如果在返回参数里直接定义会报 nil map
itemMap := make(map[string]*model.UserExample)
err := c.cache.MultiGet(ctx, keys, itemMap)
if err != nil {
return nil, err
}
retMap := make(map[string]*model.UserExample)
for _, v := range ids {
val, ok := itemMap[c.GetUserExampleCacheKey(v)]
if ok {
retMap[cast.ToString(v)] = val
}
}
return retMap, nil
}
// Del 删除cache
func (c *userExampleCache) Del(ctx context.Context, id uint64) error {
cacheKey := c.GetUserExampleCacheKey(id)
err := c.cache.Del(ctx, cacheKey)
if err != nil {
return err
}
return nil
}
// SetCacheWithNotFound 设置空缓存
func (c *userExampleCache) SetCacheWithNotFound(ctx context.Context, id uint64) error {
cacheKey := c.GetUserExampleCacheKey(id)
err := c.cache.SetCacheWithNotFound(ctx, cacheKey)
if err != nil {
return err
}
return nil
}

127
internal/cache/userExample_test.go vendored Normal file
View File

@ -0,0 +1,127 @@
package cache
import (
"context"
"testing"
"time"
"github.com/zhufuyi/sponge/internal/model"
"github.com/zhufuyi/sponge/pkg/mysql"
"github.com/alicebob/miniredis/v2"
"github.com/go-redis/redis/v8"
"github.com/stretchr/testify/assert"
)
var (
redisServer *miniredis.Miniredis
redisClient *redis.Client
testData = &model.UserExample{
Model: mysql.Model{ID: 1},
Name: "foo",
}
uc UserExampleCache
)
func setup() {
redisServer = mockRedis()
redisClient = redis.NewClient(&redis.Options{Addr: redisServer.Addr()})
uc = NewUserExampleCache(redisClient)
}
func teardown() {
redisServer.Close()
}
func mockRedis() *miniredis.Miniredis {
s, err := miniredis.Run()
if err != nil {
panic(err)
}
return s
}
func Test_userExampleCache_Set(t *testing.T) {
setup()
defer teardown()
var id uint64
ctx := context.Background()
id = 1
err := uc.Set(ctx, id, testData, time.Hour)
if err != nil {
t.Fatal(err)
}
}
func Test_userExampleCache_Get(t *testing.T) {
setup()
defer teardown()
var id uint64
ctx := context.Background()
id = 1
err := uc.Set(ctx, id, testData, time.Hour)
if err != nil {
t.Fatal(err)
}
act, err := uc.Get(ctx, id)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, testData, act)
}
func Test_userExampleCache_MultiGet(t *testing.T) {
setup()
defer teardown()
ctx := context.Background()
testData := []*model.UserExample{
{Model: mysql.Model{ID: 1}},
{Model: mysql.Model{ID: 2}},
}
err := uc.MultiSet(ctx, testData, time.Hour)
if err != nil {
t.Fatal(err)
}
expected := make(map[string]*model.UserExample)
expected["1"] = &model.UserExample{Model: mysql.Model{ID: 1}}
expected["2"] = &model.UserExample{Model: mysql.Model{ID: 2}}
act, err := uc.MultiGet(ctx, []uint64{1, 2})
if err != nil {
t.Fatal(err)
}
assert.Equal(t, expected, act)
}
func Test_userExampleCache_MultiSet(t *testing.T) {
setup()
defer teardown()
ctx := context.Background()
testData := []*model.UserExample{
{Model: mysql.Model{ID: 1}},
{Model: mysql.Model{ID: 2}},
}
err := uc.MultiSet(ctx, testData, time.Hour)
if err != nil {
t.Fatal(err)
}
}
func Test_userExampleCache_Del(t *testing.T) {
setup()
defer teardown()
var id uint64
ctx := context.Background()
id = 1
err := uc.Del(ctx, id)
if err != nil {
t.Fatal(err)
}
}

256
internal/dao/userExample.go Normal file
View File

@ -0,0 +1,256 @@
package dao
import (
"context"
"errors"
"fmt"
"time"
"github.com/zhufuyi/sponge/internal/cache"
"github.com/zhufuyi/sponge/internal/model"
cacheBase "github.com/zhufuyi/sponge/pkg/cache"
"github.com/zhufuyi/sponge/pkg/goredis"
"github.com/zhufuyi/sponge/pkg/mysql/query"
"github.com/spf13/cast"
"gorm.io/gorm"
)
var _ UserExampleDao = (*userExampleDao)(nil)
// UserExampleDao 定义dao接口
type UserExampleDao interface {
Create(ctx context.Context, table *model.UserExample) error
DeleteByID(ctx context.Context, id uint64) error
UpdateByID(ctx context.Context, table *model.UserExample) error
GetByID(ctx context.Context, id uint64) (*model.UserExample, error)
GetByColumns(ctx context.Context, params *query.Params) ([]*model.UserExample, int64, error)
}
type userExampleDao struct {
db *gorm.DB
cache cache.UserExampleCache
}
// NewUserExampleDao 创建dao接口
func NewUserExampleDao(db *gorm.DB, cache cache.UserExampleCache) UserExampleDao {
return &userExampleDao{db: db, cache: cache}
}
// Create 创建一条记录插入记录后id值被回写到table中
func (d *userExampleDao) Create(ctx context.Context, table *model.UserExample) error {
return d.db.WithContext(ctx).Create(table).Error
}
// DeleteByID 根据id删除一条记录
func (d *userExampleDao) DeleteByID(ctx context.Context, id uint64) error {
err := d.db.WithContext(ctx).Where("id = ?", id).Delete(&model.UserExample{}).Error
if err != nil {
return nil
}
// delete cache
_ = d.cache.Del(ctx, id)
return nil
}
// Deletes 根据id删除多条记录
func (d *userExampleDao) Deletes(ctx context.Context, ids []uint64) error {
err := d.db.WithContext(ctx).Where("id IN (?)", ids).Delete(&model.UserExample{}).Error
if err != nil {
return err
}
// delete cache
for _, id := range ids {
_ = d.cache.Del(ctx, id)
}
return nil
}
// UpdateByID 根据id更新记录
func (d *userExampleDao) UpdateByID(ctx context.Context, table *model.UserExample) error {
if table.ID < 1 {
return errors.New("id cannot be 0")
}
update := map[string]interface{}{}
// todo generate the update fields code to here
// delete the templates code start
if table.Name != "" {
update["name"] = table.Name
}
if table.Password != "" {
update["password"] = table.Password
}
if table.Email != "" {
update["email"] = table.Email
}
if table.Phone != "" {
update["phone"] = table.Phone
}
if table.Avatar != "" {
update["avatar"] = table.Avatar
}
if table.Age > 0 {
update["age"] = table.Age
}
if table.Gender > 0 {
update["gender"] = table.Gender
}
if table.LoginAt > 0 {
update["login_at"] = table.LoginAt
}
// delete the templates code end
err := d.db.WithContext(ctx).Model(table).Where("id = ?", table.ID).Updates(update).Error
if err != nil {
return err
}
// delete cache
_ = d.cache.Del(ctx, table.ID)
return nil
}
// GetByID 根据id获取一条记录
func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExample, error) {
record, err := d.cache.Get(ctx, id)
if errors.Is(err, cacheBase.ErrPlaceholder) {
return nil, model.ErrRecordNotFound
}
// 从mysql获取
if errors.Is(err, goredis.ErrRedisNotFound) {
table := &model.UserExample{}
err = d.db.WithContext(ctx).Where("id = ?", id).First(table).Error
if err != nil {
// if data is empty, set not found cache to prevent cache penetration(防止缓存穿透)
if err.Error() == model.ErrRecordNotFound.Error() {
err = d.cache.SetCacheWithNotFound(ctx, id)
if err != nil {
return nil, err
}
return nil, model.ErrRecordNotFound
}
return nil, err
}
if table.ID == 0 {
return nil, model.ErrRecordNotFound
}
// set cache
err = d.cache.Set(ctx, id, table, cacheBase.DefaultExpireTime)
if err != nil {
return nil, fmt.Errorf("cache.Set error: %v, id=%d", err, id)
}
return table, nil
}
if err != nil {
// fail fast, if cache error return, don't request to db
return nil, err
}
return record, nil
}
// GetByColumns 根据分页和列信息筛选多条记录
// params 包括分页参数和查询参数
// 分页参数(必须):
// page: 页码从0开始
// size: 每页行数
// sort: 排序字段默认是id倒叙可以在字段前添加-号表示倒序,没有-号表示升序,多个字段用逗号分隔
//
// 查询参数(非必须):
//
// name: 列名
// exp: 表达式,有=、!=、>、>=、<、<=、like七种类型值为空时默认是=
// value: 列值
// logic: 表示逻辑类型,有&(and)、||(or)两种类型值为空时默认是and
//
// 示例: 查询年龄大于20的男性
//
// params = &query.Params{
// Page: 0,
// Size: 20,
// Columns: []query.Column{
// {
// serviceName: "age",
// Exp: ">",
// Value: 20,
// },
// {
// serviceName: "gender",
// Value: "男",
// },
// }
func (d *userExampleDao) GetByColumns(ctx context.Context, params *query.Params) ([]*model.UserExample, int64, error) {
query, args, err := params.ConvertToGormConditions()
if err != nil {
return nil, 0, err
}
var total int64
err = d.db.WithContext(ctx).Model(&model.UserExample{}).Where(query, args...).Count(&total).Error
if err != nil {
return nil, 0, err
}
if total == 0 {
return nil, total, nil
}
records := []*model.UserExample{}
order, limit, offset := params.ConvertToPage()
err = d.db.WithContext(ctx).Order(order).Limit(limit).Offset(offset).Where(query, args...).Find(&records).Error
if err != nil {
return nil, 0, err
}
return records, total, err
}
// GetByIDs 根据id批量获取
func (d *userExampleDao) GetByIDs(ctx context.Context, ids []uint64) ([]*model.UserExample, error) {
records := []*model.UserExample{}
itemMap, err := d.cache.MultiGet(ctx, ids)
if err != nil {
return nil, err
}
var missedID []uint64
for _, id := range ids {
item, ok := itemMap[cast.ToString(id)]
if !ok {
missedID = append(missedID, id)
continue
}
records = append(records, item)
}
// get missed data
if len(missedID) > 0 {
var missedData []*model.UserExample
err = d.db.WithContext(ctx).Where("id IN (?)", missedID).Find(&missedData).Error
if err != nil {
return nil, err
}
if len(missedData) > 0 {
records = append(records, missedData...)
err = d.cache.MultiSet(ctx, missedData, 10*time.Minute)
if err != nil {
return nil, err
}
}
}
return records, nil
}

View File

@ -0,0 +1,25 @@
package ecode
import (
"github.com/zhufuyi/sponge/pkg/errcode"
)
// nolint
// rpc系统级别错误码有status前缀
var (
StatusSuccess = errcode.StatusSuccess
StatusInvalidParams = errcode.StatusInvalidParams
StatusUnauthorized = errcode.StatusUnauthorized
StatusInternalServerError = errcode.StatusInternalServerError
StatusNotFound = errcode.StatusNotFound
StatusAlreadyExists = errcode.StatusAlreadyExists
StatusTimeout = errcode.StatusTimeout
StatusTooManyRequests = errcode.StatusTooManyRequests
StatusForbidden = errcode.StatusForbidden
StatusLimitExceed = errcode.StatusLimitExceed
StatusDeadlineExceeded = errcode.StatusDeadlineExceeded
StatusAccessDenied = errcode.StatusAccessDenied
StatusMethodNotAllowed = errcode.StatusMethodNotAllowed
)

View File

@ -0,0 +1,22 @@
package ecode
import "github.com/zhufuyi/sponge/pkg/errcode"
const (
// todo must be modified manually
// 每个资源名称对应唯一编号编号范围1~1000如果存在编号相同启动服务会报错
_userExampleNO = 1
// userExample对应的中文名称
_userExampleName = "userExample_cn_name"
)
// nolint
// 服务级别错误码有Err前缀
var (
StatusCreateUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+1, "创建"+_userExampleName+"失败") // todo 补充错误码注释,例如 20101
StatusDeleteUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+2, "删除"+_userExampleName+"失败")
StatusUpdateUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+3, "更新"+_userExampleName+"失败")
StatusGetUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+4, "获取"+_userExampleName+"失败")
StatusListUserExample = errcode.NewGRPCStatus(errcode.GCode(_userExampleNO)+5, "获取"+_userExampleName+"列表失败")
// 每添加一个错误码,在上一个错误码基础上+1
)

View File

@ -0,0 +1,22 @@
package ecode
import "github.com/zhufuyi/sponge/pkg/errcode"
// nolint
// http系统级别错误码无Err前缀错误码小于20000
var (
Success = errcode.Success
InvalidParams = errcode.InvalidParams
Unauthorized = errcode.Unauthorized
InternalServerError = errcode.InternalServerError
NotFound = errcode.NotFound
AlreadyExists = errcode.AlreadyExists
Timeout = errcode.Timeout
TooManyRequests = errcode.TooManyRequests
Forbidden = errcode.Forbidden
LimitExceed = errcode.LimitExceed
DeadlineExceeded = errcode.DeadlineExceeded
AccessDenied = errcode.AccessDenied
MethodNotAllowed = errcode.MethodNotAllowed
)

View File

@ -0,0 +1,22 @@
package ecode
import "github.com/zhufuyi/sponge/pkg/errcode"
const (
// todo must be modified manually
// 每个资源名称对应唯一编号编号范围1~1000如果存在编号相同启动服务会报错
userExampleNO = 1
// userExample对应的中文名称
userExampleName = "userExample_cn_name"
)
// nolint
// 服务级别错误码有Err前缀
var (
ErrCreateUserExample = errcode.NewError(errcode.HCode(userExampleNO)+1, "创建"+userExampleName+"失败") // todo 补充错误码注释,例如 20101
ErrDeleteUserExample = errcode.NewError(errcode.HCode(userExampleNO)+2, "删除"+userExampleName+"失败")
ErrUpdateUserExample = errcode.NewError(errcode.HCode(userExampleNO)+3, "更新"+userExampleName+"失败")
ErrGetUserExample = errcode.NewError(errcode.HCode(userExampleNO)+4, "获取"+userExampleName+"失败")
ErrListUserExample = errcode.NewError(errcode.HCode(userExampleNO)+5, "获取"+userExampleName+"列表失败")
// 每添加一个错误码,在上一个错误码基础上+1
)

View File

@ -0,0 +1,28 @@
package handler
// swagger公共结构体每个字段建议都写注释生成的swagger.json也带有注释
// 把swagger.json导入yapi后自动填写备注避免重复填写
// Result 输出数据格式
type Result struct {
Code int `json:"code"` // 返回码
Msg string `json:"msg"` // 返回信息说明
Data interface{} `json:"data"` // 返回数据
}
// Params 查询原始参数
type Params struct {
Page int `form:"page" binding:"gte=0" json:"page"` // 页码
Size int `form:"size" binding:"gt=0" json:"size"` // 每页行数
Sort string `form:"sort" binding:"" json:"sort,omitempty"` // 排序字段,默认值为-id字段前面有-号表示倒序,否则升序,多个字段用逗号分隔
Columns []Column `json:"columns,omitempty"` // 列查询条件
}
// Column 表的列查询信息
type Column struct {
Name string `json:"name"` // 列名
Exp string `json:"exp"` // 表达式,值为空时默认为=,有=、!=、>、>=、<、<=、like七种类型
Value interface{} `json:"value"` // 列值
Logic string `json:"logic"` // 逻辑类型值为空时默认为and有&(and)、||(or)两种类型
}

View File

@ -0,0 +1,307 @@
package handler
import (
"time"
"github.com/zhufuyi/sponge/internal/cache"
"github.com/zhufuyi/sponge/internal/dao"
"github.com/zhufuyi/sponge/internal/ecode"
"github.com/zhufuyi/sponge/internal/model"
"github.com/zhufuyi/sponge/pkg/gin/response"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/zhufuyi/sponge/pkg/mysql/query"
"github.com/zhufuyi/sponge/pkg/utils"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
)
var _ UserExampleHandler = (*userExampleHandler)(nil)
// UserExampleHandler 定义handler接口
type UserExampleHandler interface {
Create(c *gin.Context)
DeleteByID(c *gin.Context)
UpdateByID(c *gin.Context)
GetByID(c *gin.Context)
List(c *gin.Context)
}
type userExampleHandler struct {
iDao dao.UserExampleDao
}
// NewUserExampleHandler 创建handler接口
func NewUserExampleHandler() UserExampleHandler {
return &userExampleHandler{
iDao: dao.NewUserExampleDao(
model.GetDB(),
cache.NewUserExampleCache(model.GetRedisCli()),
),
}
}
// Create 创建
// @Summary 创建userExample
// @Description 提交信息创建userExample
// @Tags userExample
// @accept json
// @Produce json
// @Param data body CreateUserExampleRequest true "userExample信息"
// @Success 200 {object} Result{}
// @Router /api/v1/userExample [post]
func (h *userExampleHandler) Create(c *gin.Context) {
form := &CreateUserExampleRequest{}
err := c.ShouldBindJSON(form)
if err != nil {
logger.Warn("ShouldBindJSON error: ", logger.Err(err), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InvalidParams)
return
}
userExample := &model.UserExample{}
err = copier.Copy(userExample, form)
if err != nil {
logger.Error("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InternalServerError)
return
}
err = h.iDao.Create(c.Request.Context(), userExample)
if err != nil {
logger.Error("Create error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrCreateUserExample)
return
}
response.Success(c, gin.H{"id": userExample.ID})
}
// DeleteByID 根据id删除一条记录
// @Summary 删除userExample
// @Description 根据id删除userExample
// @Tags userExample
// @accept json
// @Produce json
// @Param id path string true "id"
// @Success 200 {object} Result{}
// @Router /api/v1/userExample/{id} [delete]
func (h *userExampleHandler) DeleteByID(c *gin.Context) {
_, id, isAbout := getUserExampleIDFromPath(c)
if isAbout {
return
}
err := h.iDao.DeleteByID(c.Request.Context(), id)
if err != nil {
logger.Error("DeleteByID error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrDeleteUserExample)
return
}
response.Success(c)
}
// UpdateByID 根据id更新信息
// @Summary 更新userExample信息
// @Description 根据id更新userExample信息
// @Tags userExample
// @accept json
// @Produce json
// @Param id path string true "id"
// @Param data body UpdateUserExampleByIDRequest true "userExample信息"
// @Success 200 {object} Result{}
// @Router /api/v1/userExample/{id} [put]
func (h *userExampleHandler) UpdateByID(c *gin.Context) {
_, id, isAbout := getUserExampleIDFromPath(c)
if isAbout {
return
}
form := &UpdateUserExampleByIDRequest{}
err := c.ShouldBindJSON(form)
if err != nil {
logger.Warn("ShouldBindJSON error: ", logger.Err(err), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InvalidParams)
return
}
form.ID = id
userExample := &model.UserExample{}
err = copier.Copy(userExample, form)
if err != nil {
logger.Error("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InternalServerError)
return
}
err = h.iDao.UpdateByID(c.Request.Context(), userExample)
if err != nil {
logger.Error("UpdateByID error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrUpdateUserExample)
return
}
response.Success(c)
}
// GetByID 根据id获取一条记录
// @Summary 获取userExample详情
// @Description 根据id获取userExample详情
// @Tags userExample
// @Param id path string true "id"
// @Accept json
// @Produce json
// @Success 200 {object} Result{}
// @Router /api/v1/userExample/{id} [get]
func (h *userExampleHandler) GetByID(c *gin.Context) {
idstr, id, isAbout := getUserExampleIDFromPath(c)
if isAbout {
return
}
userExample, err := h.iDao.GetByID(c.Request.Context(), id)
if err != nil {
if err.Error() == query.ErrNotFound.Error() {
logger.Warn("GetByID not found", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.NotFound)
} else {
logger.Error("GetByID error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrGetUserExample)
}
return
}
data := &GetUserExampleByIDRespond{}
err = copier.Copy(data, userExample)
if err != nil {
logger.Error("Copy error", logger.Err(err), logger.Any("id", id), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InternalServerError)
return
}
data.ID = idstr
response.Success(c, gin.H{"userExample": data})
}
// List 通过post获取多条记录
// @Summary 获取userExample列表
// @Description 使用post请求获取userExample列表
// @Tags userExample
// @accept json
// @Produce json
// @Param data body Params true "查询条件"
// @Success 200 {object} Result{}
// @Router /api/v1/userExamples [post]
func (h *userExampleHandler) List(c *gin.Context) {
form := &GetUserExamplesRequest{}
err := c.ShouldBindJSON(form)
if err != nil {
logger.Warn("ShouldBindJSON error: ", logger.Err(err), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InvalidParams)
return
}
userExamples, total, err := h.iDao.GetByColumns(c.Request.Context(), &form.Params)
if err != nil {
logger.Error("GetByColumns error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.ErrListUserExample)
return
}
data, err := convertUserExamples(userExamples)
if err != nil {
logger.Error("Copy error", logger.Err(err), logger.Any("form", form), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InternalServerError)
return
}
response.Success(c, gin.H{
"userExamples": data,
"total": total,
})
}
// ----------------------------------- 定义请求参数和返回结果 -----------------------------
// todo generate the request and response struct to here
// delete the templates code start
// CreateUserExampleRequest 创建请求参数所有字段是必须的并且满足binding规则
// binding使用说明 https://github.com/go-playground/validator
type CreateUserExampleRequest struct {
Name string `json:"name" binding:"min=2"` // 名称
Email string `json:"email" binding:"email"` // 邮件
Password string `json:"password" binding:"md5"` // 密码
Phone string `form:"phone" binding:"e164"` // 手机号码,必须在前加'+86'
Avatar string `form:"avatar" binding:"min=5"` // 头像
Age int `form:"age" binding:"gt=0,lt=120"` // 年龄
Gender int `form:"gender" binding:"gte=0,lte=2"` // 性别1:男2:女
}
// UpdateUserExampleByIDRequest 更新请求参数,所有字段不是必须的,字段为非零值更新
type UpdateUserExampleByIDRequest struct {
ID uint64 `json:"id" binding:"-"` // id
Name string `json:"name" binding:""` // 名称
Email string `json:"email" binding:""` // 邮件
Password string `json:"password" binding:""` // 密码
Phone string `form:"phone" binding:""` // 手机号码,必须在前加'+86'
Avatar string `form:"avatar" binding:""` // 头像
Age int `form:"age" binding:""` // 年龄
Gender int `form:"gender" binding:""` // 性别1:男2:女
}
// GetUserExampleByIDRespond 返回数据
type GetUserExampleByIDRespond struct {
ID string `json:"id"` // id
Name string `json:"name"` // 名称
Email string `json:"email"` // 邮件
Phone string `json:"phone"` // 手机号码
Avatar string `json:"avatar"` // 头像
Age int `json:"age"` // 年龄
Gender int `json:"gender"` // 性别1:男2:女
Status int `json:"status"` // 账号状态
LoginAt int64 `json:"login_at"` // 登录时间戳
CreatedAt time.Time `json:"created_at"` // 创建时间
UpdatedAt time.Time `json:"updated_at"` // 更新时间
}
// delete the templates code end
// GetUserExamplesRequest query params
type GetUserExamplesRequest struct {
query.Params
}
// ListUserExamplesRespond list data
type ListUserExamplesRespond []struct {
GetUserExampleByIDRespond
}
// ------------------------------- 除了handler的辅助函数 -----------------------------
func getUserExampleIDFromPath(c *gin.Context) (string, uint64, bool) {
idStr := c.Param("id")
id, err := utils.StrToUint64E(idStr)
if err != nil || id == 0 {
logger.Warn("StrToUint64E error: ", logger.String("idStr", idStr), utils.FieldRequestIDFromContext(c))
response.Error(c, ecode.InvalidParams)
return "", 0, true
}
return idStr, id, false
}
func convertUserExamples(fromValues []*model.UserExample) ([]*GetUserExampleByIDRespond, error) {
toValues := []*GetUserExampleByIDRespond{}
for _, v := range fromValues {
data := &GetUserExampleByIDRespond{}
err := copier.Copy(data, v)
if err != nil {
return nil, err
}
data.ID = utils.Uint64ToStr(v.ID)
toValues = append(toValues, data)
}
return toValues, nil
}

110
internal/model/init.go Normal file
View File

@ -0,0 +1,110 @@
package model
import (
"sync"
"time"
"github.com/zhufuyi/sponge/config"
"github.com/zhufuyi/sponge/pkg/goredis"
"github.com/zhufuyi/sponge/pkg/mysql"
"github.com/go-redis/redis/v8"
"gorm.io/gorm"
)
var (
// ErrRecordNotFound 没有找到记录
ErrRecordNotFound = gorm.ErrRecordNotFound
)
var (
db *gorm.DB
once1 sync.Once
redisCli *redis.Client
once2 sync.Once
)
// InitMysql 连接mysql
func InitMysql() {
opts := []mysql.Option{
mysql.WithSlowThreshold(time.Duration(config.Get().Mysql.SlowThreshold) * time.Millisecond),
mysql.WithMaxIdleConns(config.Get().Mysql.MaxIdleConns),
mysql.WithMaxOpenConns(config.Get().Mysql.MaxOpenConns),
mysql.WithConnMaxLifetime(time.Duration(config.Get().Mysql.ConnMaxLifetime) * time.Minute),
}
if config.Get().Mysql.EnableLog {
opts = append(opts, mysql.WithLog())
}
if config.Get().App.EnableTracing {
opts = append(opts, mysql.WithEnableTrace())
}
var err error
db, err = mysql.Init(config.Get().Mysql.Dsn, opts...)
if err != nil {
panic("mysql.Init error: " + err.Error())
}
}
// GetDB 返回db对象
func GetDB() *gorm.DB {
if db == nil {
once1.Do(func() {
InitMysql()
})
}
return db
}
// CloseMysql 关闭mysql
func CloseMysql() error {
if db != nil {
sqlDB, err := db.DB()
if err != nil {
return err
}
if sqlDB != nil {
return sqlDB.Close()
}
}
return nil
}
// InitRedis 连接redis
func InitRedis() {
opts := []goredis.Option{}
if config.Get().App.EnableTracing {
opts = append(opts, goredis.WithEnableTrace())
}
var err error
redisCli, err = goredis.Init(config.Get().Redis.Dsn, opts...)
if err != nil {
panic("goredis.Init error: " + err.Error())
}
}
// GetRedisCli 返回redis client
func GetRedisCli() *redis.Client {
if redisCli == nil {
once2.Do(func() {
InitRedis()
})
}
return redisCli
}
// CloseRedis 关闭redis
func CloseRedis() error {
err := redisCli.Close()
if err != nil && err.Error() != redis.ErrClosed.Error() {
return err
}
return nil
}

View File

@ -0,0 +1,30 @@
// todo generate model codes to here
// delete the templates code start
package model
import (
"github.com/zhufuyi/sponge/pkg/mysql"
)
// UserExample object fields mapping table
type UserExample struct {
mysql.Model `gorm:"embedded"`
Name string `gorm:"column:name;NOT NULL" json:"name"` // 用户名
Password string `gorm:"column:password;NOT NULL" json:"password"` // 密码
Email string `gorm:"column:email;NOT NULL" json:"email"` // 邮件
Phone string `gorm:"column:phone;NOT NULL" json:"phone"` // 手机号码
Avatar string `gorm:"column:avatar;NOT NULL" json:"avatar"` // 头像
Age int `gorm:"column:age;NOT NULL" json:"age"` // 年龄
Gender int `gorm:"column:gender;NOT NULL" json:"gender"` // 性别1:男2:女,其他值:未知
Status int `gorm:"column:status;NOT NULL" json:"status"` // 账号状态1:未激活2:已激活3:封禁
LoginAt int64 `gorm:"column:login_at;NOT NULL" json:"login_at"` // 登录时间戳
}
// TableName get table name
func (table *UserExample) TableName() string {
return "user_example"
}
// delete the templates code end

View File

@ -0,0 +1,93 @@
package routers
import (
"net/http"
"strings"
"github.com/zhufuyi/sponge/config"
"github.com/zhufuyi/sponge/docs"
"github.com/zhufuyi/sponge/pkg/gin/handlerfunc"
"github.com/zhufuyi/sponge/pkg/gin/middleware"
"github.com/zhufuyi/sponge/pkg/gin/middleware/metrics"
"github.com/zhufuyi/sponge/pkg/gin/middleware/ratelimiter"
"github.com/zhufuyi/sponge/pkg/gin/validator"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
swaggerFiles "github.com/swaggo/files"
ginSwagger "github.com/swaggo/gin-swagger"
)
var (
routerFns []func() // 路由集合
apiV1 *gin.RouterGroup // 基础路由组
)
// NewRouter 实例化路由
func NewRouter() *gin.Engine {
r := gin.New()
r.Use(gin.Recovery())
r.Use(middleware.Cors())
// request id 中间件
r.Use(middleware.RequestID())
// logger 中间件
r.Use(middleware.Logging(
middleware.WithLog(logger.Get()),
middleware.WithRequestIDFromContext(),
middleware.WithIgnoreRoutes("/metrics"), // 忽略路由
))
// metrics 中间件
if config.Get().App.EnableMetrics {
r.Use(metrics.Metrics(r,
//metrics.WithMetricsPath("/metrics"), // 默认是 /metrics
metrics.WithIgnoreStatusCodes(http.StatusNotFound), // 忽略404状态码
))
}
// limit 中间件
if config.Get().App.EnableLimit {
opts := []ratelimiter.Option{
ratelimiter.WithQPS(config.Get().RateLimiter.QPSLimit),
ratelimiter.WithBurst(config.Get().RateLimiter.MaxLimit),
}
if strings.ToUpper(config.Get().RateLimiter.Dimension) == "IP" {
opts = append(opts, ratelimiter.WithIP())
}
r.Use(ratelimiter.QPS(opts...))
}
// trace 中间件
if config.Get().App.EnableTracing {
r.Use(middleware.Tracing(config.Get().App.Name))
}
// profile 性能分析
if config.Get().App.EnableProfile {
pprof.Register(r)
}
// 校验器
binding.Validator = validator.Init()
// 注册swagger路由通过swag init生成代码
docs.SwaggerInfo.BasePath = ""
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
r.GET("/health", handlerfunc.CheckHealth)
r.GET("/ping", handlerfunc.Ping)
apiV1 = r.Group("/api/v1")
// 注册所有路由
for _, fn := range routerFns {
fn()
}
return r
}

View File

@ -0,0 +1,22 @@
package routers
import (
"github.com/zhufuyi/sponge/internal/handler"
"github.com/gin-gonic/gin"
)
// nolint
func init() {
routerFns = append(routerFns, func() {
userExampleRouter(apiV1, handler.NewUserExampleHandler()) // 加入到路由组
})
}
func userExampleRouter(group *gin.RouterGroup, h handler.UserExampleHandler) {
group.POST("/userExample", h.Create)
group.DELETE("/userExample/:id", h.DeleteByID)
group.PUT("/userExample/:id", h.UpdateByID)
group.GET("/userExample/:id", h.GetByID)
group.POST("/userExamples", h.List) // 通过post任意列组合查询
}

159
internal/server/grpc.go Normal file
View File

@ -0,0 +1,159 @@
package server
import (
"context"
"errors"
"fmt"
"net"
"net/http"
"time"
"github.com/zhufuyi/sponge/config"
"github.com/zhufuyi/sponge/internal/service"
"github.com/zhufuyi/sponge/pkg/app"
"github.com/zhufuyi/sponge/pkg/grpc/interceptor"
"github.com/zhufuyi/sponge/pkg/grpc/metrics"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/zhufuyi/sponge/pkg/registry"
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
"google.golang.org/grpc"
)
var _ app.IServer = (*grpcServer)(nil)
type grpcServer struct {
addr string
server *grpc.Server
listen net.Listener
metricsHTTPServer *http.Server
goRunPromHTTPServer func() error
iRegistry registry.Registry
serviceInstance *registry.ServiceInstance
}
// Start grpc service
func (s *grpcServer) Start() error {
if s.iRegistry != nil {
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint
if err := s.iRegistry.Register(ctx, s.serviceInstance); err != nil {
return err
}
}
if s.goRunPromHTTPServer != nil {
if err := s.goRunPromHTTPServer(); err != nil {
return err
}
}
if err := s.server.Serve(s.listen); err != nil { // block
return err
}
return nil
}
// Stop grpc service
func (s *grpcServer) Stop() error {
if s.iRegistry != nil {
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint
if err := s.iRegistry.Deregister(ctx, s.serviceInstance); err != nil {
return err
}
}
s.server.GracefulStop()
if s.metricsHTTPServer != nil {
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint
if err := s.metricsHTTPServer.Shutdown(ctx); err != nil {
return err
}
}
return nil
}
// String comment
func (s *grpcServer) String() string {
return "grpc service, addr = " + s.addr
}
// InitServerOptions 初始化rpc设置
func (s *grpcServer) serverOptions() []grpc.ServerOption {
var options []grpc.ServerOption
unaryServerInterceptors := []grpc.UnaryServerInterceptor{
interceptor.UnaryServerRecovery(),
interceptor.UnaryServerCtxTags(),
}
streamServerInterceptors := []grpc.StreamServerInterceptor{}
// logger 拦截器
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerLog(
logger.Get(),
))
// metrics 拦截器
if config.Get().App.EnableMetrics {
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerMetrics())
s.goRunPromHTTPServer = func() error {
if s == nil || s.server == nil {
return errors.New("grpc server is nil")
}
promAddr := fmt.Sprintf(":%d", config.Get().Metrics.Port)
s.metricsHTTPServer = metrics.GoHTTPService(promAddr, s.server)
logger.Infof("start up prometheus http service, addr = %s", promAddr)
return nil
}
}
// limit 拦截器
if config.Get().App.EnableLimit {
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerRateLimit(
interceptor.WithRateLimitQPS(config.Get().RateLimiter.QPSLimit),
))
}
// trace 拦截器
if config.Get().App.EnableTracing {
unaryServerInterceptors = append(unaryServerInterceptors, interceptor.UnaryServerTracing())
}
unaryServer := grpc_middleware.WithUnaryServerChain(unaryServerInterceptors...)
streamServer := grpc_middleware.WithStreamServerChain(streamServerInterceptors...)
options = append(options, unaryServer, streamServer)
return options
}
// NewGRPCServer 创建一个grpc服务
func NewGRPCServer(addr string, opts ...GRPCOption) app.IServer {
var err error
o := defaultGRPCOptions()
o.apply(opts...)
s := &grpcServer{
addr: addr,
iRegistry: o.iRegistry,
serviceInstance: o.instance,
}
// 监听TCP端口
s.listen, err = net.Listen("tcp", addr)
if err != nil {
panic(err)
}
// 创建grpc server对象拦截器可以在这里注入
s.server = grpc.NewServer(s.serverOptions()...)
// 注册所有服务
service.RegisterAllService(s.server)
return s
}

View File

@ -0,0 +1,55 @@
package server
import (
"time"
"github.com/zhufuyi/sponge/pkg/registry"
)
// GRPCOption 设置grpc
type GRPCOption func(*grpcOptions)
type grpcOptions struct {
readTimeout time.Duration
writeTimeout time.Duration
instance *registry.ServiceInstance
iRegistry registry.Registry
}
// 默认设置
func defaultGRPCOptions() *grpcOptions {
return &grpcOptions{
readTimeout: time.Second * 3,
writeTimeout: time.Second * 3,
instance: nil,
iRegistry: nil,
}
}
func (o *grpcOptions) apply(opts ...GRPCOption) {
for _, opt := range opts {
opt(o)
}
}
// WithGRPCReadTimeout 设置read timeout
func WithGRPCReadTimeout(timeout time.Duration) GRPCOption {
return func(o *grpcOptions) {
o.readTimeout = timeout
}
}
// WithGRPCWriteTimeout 设置writer timeout
func WithGRPCWriteTimeout(timeout time.Duration) GRPCOption {
return func(o *grpcOptions) {
o.writeTimeout = timeout
}
}
// WithRegistry 设置registry
func WithRegistry(iRegistry registry.Registry, instance *registry.ServiceInstance) GRPCOption {
return func(o *grpcOptions) {
o.iRegistry = iRegistry
o.instance = instance
}
}

65
internal/server/http.go Normal file
View File

@ -0,0 +1,65 @@
package server
import (
"context"
"fmt"
"net/http"
"time"
"github.com/zhufuyi/sponge/internal/routers"
"github.com/zhufuyi/sponge/pkg/app"
"github.com/gin-gonic/gin"
)
var _ app.IServer = (*httpServer)(nil)
type httpServer struct {
addr string
server *http.Server
}
// Start http service
func (s *httpServer) Start() error {
if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("listen server error: %v", err)
}
return nil
}
// Stop http service
func (s *httpServer) Stop() error {
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second) //nolint
return s.server.Shutdown(ctx)
}
// String comment
func (s *httpServer) String() string {
return "http service, addr = " + s.addr
}
// NewHTTPServer creates a new web server
func NewHTTPServer(addr string, opts ...HTTPOption) app.IServer {
o := defaultHTTPOptions()
o.apply(opts...)
if o.isProd {
gin.SetMode(gin.ReleaseMode)
} else {
gin.SetMode(gin.DebugMode)
}
router := routers.NewRouter()
server := &http.Server{
Addr: addr,
Handler: router,
ReadTimeout: o.readTimeout,
WriteTimeout: o.writeTimeout,
MaxHeaderBytes: 1 << 20,
}
return &httpServer{
addr: addr,
server: server,
}
}

View File

@ -0,0 +1,50 @@
package server
import (
"time"
)
// HTTPOption 设置http
type HTTPOption func(*httpOptions)
type httpOptions struct {
readTimeout time.Duration
writeTimeout time.Duration
isProd bool
}
// 默认设置
func defaultHTTPOptions() *httpOptions {
return &httpOptions{
readTimeout: time.Second * 60,
writeTimeout: time.Second * 60,
isProd: false,
}
}
func (o *httpOptions) apply(opts ...HTTPOption) {
for _, opt := range opts {
opt(o)
}
}
// WithHTTPReadTimeout 设置read timeout
func WithHTTPReadTimeout(timeout time.Duration) HTTPOption {
return func(o *httpOptions) {
o.readTimeout = timeout
}
}
// WithHTTPWriteTimeout 设置writer timeout
func WithHTTPWriteTimeout(timeout time.Duration) HTTPOption {
return func(o *httpOptions) {
o.writeTimeout = timeout
}
}
// WithHTTPIsProd 设置是否为生产环境
func WithHTTPIsProd(IsProd bool) HTTPOption {
return func(o *httpOptions) {
o.isProd = IsProd
}
}

View File

@ -0,0 +1,21 @@
package service
import (
"google.golang.org/grpc"
"google.golang.org/grpc/health"
healthPB "google.golang.org/grpc/health/grpc_health_v1"
)
var (
// registerFns 注册方法集合
registerFns []func(server *grpc.Server)
)
// RegisterAllService 注册所有service到服务中
func RegisterAllService(server *grpc.Server) {
healthPB.RegisterHealthServer(server, health.NewServer()) // 注册健康检测
for _, fn := range registerFns {
fn(server)
}
}

View File

@ -0,0 +1,179 @@
package service
import (
"context"
pb "github.com/zhufuyi/sponge/api/userExample/v1"
"github.com/zhufuyi/sponge/internal/cache"
"github.com/zhufuyi/sponge/internal/dao"
"github.com/zhufuyi/sponge/internal/ecode"
"github.com/zhufuyi/sponge/internal/model"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/zhufuyi/sponge/pkg/mysql/query"
"github.com/jinzhu/copier"
"google.golang.org/grpc"
)
// nolint
func init() {
registerFns = append(registerFns, func(server *grpc.Server) {
pb.RegisterUserExampleServiceServer(server, NewUserExampleServiceServer()) // 把service注册到rpc服务中
})
}
var _ pb.UserExampleServiceServer = (*userExampleService)(nil)
type userExampleService struct {
pb.UnimplementedUserExampleServiceServer
iDao dao.UserExampleDao
}
// NewUserExampleServiceServer 创建一个实例
func NewUserExampleServiceServer() pb.UserExampleServiceServer {
return &userExampleService{
iDao: dao.NewUserExampleDao(
model.GetDB(),
cache.NewUserExampleCache(model.GetRedisCli()),
),
}
}
func (s *userExampleService) Create(ctx context.Context, req *pb.CreateUserExampleRequest) (*pb.CreateUserExampleReply, error) {
err := req.Validate()
if err != nil {
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
return nil, ecode.StatusInvalidParams.Err()
}
userExample := &model.UserExample{}
err = copier.Copy(userExample, req)
if err != nil {
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("req", req))
return nil, ecode.StatusInternalServerError.Err()
}
err = s.iDao.Create(ctx, userExample)
if err != nil {
logger.Error("s.iDao.Create error", logger.Err(err), logger.Any("userExample", userExample))
return nil, ecode.StatusCreateUserExample.Err()
}
return &pb.CreateUserExampleReply{Id: userExample.ID}, nil
}
func (s *userExampleService) DeleteByID(ctx context.Context, req *pb.DeleteUserExampleByIDRequest) (*pb.DeleteUserExampleByIDReply, error) {
err := req.Validate()
if err != nil {
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
return nil, ecode.StatusInvalidParams.Err()
}
err = s.iDao.DeleteByID(ctx, req.Id)
if err != nil {
logger.Error("s.iDao.DeleteByID error", logger.Err(err), logger.Any("id", req.Id))
return nil, ecode.StatusDeleteUserExample.Err()
}
return &pb.DeleteUserExampleByIDReply{}, nil
}
func (s *userExampleService) UpdateByID(ctx context.Context, req *pb.UpdateUserExampleByIDRequest) (*pb.UpdateUserExampleByIDReply, error) {
err := req.Validate()
if err != nil {
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
return nil, ecode.StatusInvalidParams.Err()
}
userExample := &model.UserExample{}
err = copier.Copy(userExample, req)
if err != nil {
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("req", req))
return nil, ecode.StatusInternalServerError.Err()
}
userExample.ID = req.Id
err = s.iDao.UpdateByID(ctx, userExample)
if err != nil {
logger.Error("s.iDao.UpdateByID error", logger.Err(err), logger.Any("userExample", userExample))
return nil, ecode.StatusUpdateUserExample.Err()
}
return &pb.UpdateUserExampleByIDReply{}, nil
}
func (s *userExampleService) GetByID(ctx context.Context, req *pb.GetUserExampleByIDRequest) (*pb.GetUserExampleByIDReply, error) {
err := req.Validate()
if err != nil {
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
return nil, ecode.StatusInvalidParams.Err()
}
record, err := s.iDao.GetByID(ctx, req.Id)
if err != nil {
if err.Error() == query.ErrNotFound.Error() {
logger.Warn("s.iDao.GetByID error", logger.Err(err), logger.Any("id", req.Id))
return nil, ecode.StatusNotFound.Err()
}
logger.Error("s.iDao.GetByID error", logger.Err(err), logger.Any("id", req.Id))
return nil, ecode.StatusGetUserExample.Err()
}
value, err := covertUserExample(record)
if err != nil {
logger.Warn("covertUserExample error", logger.Err(err), logger.Any("record", record))
return nil, ecode.StatusInternalServerError.Err()
}
return &pb.GetUserExampleByIDReply{UserExample: value}, nil
}
func (s *userExampleService) List(ctx context.Context, req *pb.ListUserExampleRequest) (*pb.ListUserExampleReply, error) {
err := req.Validate()
if err != nil {
logger.Warn("req.Validate error", logger.Err(err), logger.Any("req", req))
return nil, ecode.StatusInvalidParams.Err()
}
params := &query.Params{}
err = copier.Copy(params, req.Params)
if err != nil {
logger.Warn("copier.Copy error", logger.Err(err), logger.Any("params", req.Params))
return nil, ecode.StatusInternalServerError.Err()
}
params.Size = int(req.Params.Limit)
records, total, err := s.iDao.GetByColumns(ctx, params)
if err != nil {
logger.Error("s.iDao.GetByColumns error", logger.Err(err), logger.Any("params", params))
return nil, ecode.StatusListUserExample.Err()
}
values := []*pb.UserExample{}
for _, record := range records {
value, err := covertUserExample(record)
if err != nil {
logger.Warn("covertUserExample error", logger.Err(err), logger.Any("id", record.ID))
continue
}
values = append(values, value)
}
return &pb.ListUserExampleReply{
Total: total,
UserExamples: values,
}, nil
}
func covertUserExample(record *model.UserExample) (*pb.UserExample, error) {
value := &pb.UserExample{}
err := copier.Copy(value, record)
if err != nil {
return nil, err
}
value.Id = record.ID
value.CreatedAt = record.CreatedAt.UnixNano()
value.UpdatedAt = record.UpdatedAt.UnixNano()
return value, nil
}

View File

@ -0,0 +1,207 @@
package service
import (
"context"
"fmt"
"testing"
"github.com/zhufuyi/sponge/api/types"
pb "github.com/zhufuyi/sponge/api/userExample/v1"
"github.com/zhufuyi/sponge/config"
"github.com/zhufuyi/sponge/pkg/grpc/benchmark"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func initUserExampleServiceClient() pb.UserExampleServiceClient {
err := config.Init(config.Path("conf.yml"))
if err != nil {
panic(err)
}
addr := fmt.Sprintf("127.0.0.1:%d", config.Get().Grpc.Port)
conn, err := grpc.Dial(addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
panic(err)
}
//defer conn.Close()
return pb.NewUserExampleServiceClient(conn)
}
// 通过客户端测试userExample的各个方法
func Test_userExampleService_methods(t *testing.T) {
cli := initUserExampleServiceClient()
ctx := context.Background()
tests := []struct {
name string
fn func() (interface{}, error)
wantErr bool
}{
// todo generate the service struct code here
// delete the templates code start
{
name: "Create",
fn: func() (interface{}, error) {
// todo test after filling in parameters
return cli.Create(ctx, &pb.CreateUserExampleRequest{
Name: "宋九",
Email: "foo7@bar.com",
Password: "f447b20a7fcbf53a5d5be013ea0b15af",
Phone: "+8618576552066",
Avatar: "http://internal.com/7.jpg",
Age: 21,
Gender: 2,
})
},
wantErr: false,
},
{
name: "UpdateByID",
fn: func() (interface{}, error) {
// todo test after filling in parameters
return cli.UpdateByID(ctx, &pb.UpdateUserExampleByIDRequest{
Id: 7,
Phone: "18666666666",
Age: 21,
})
},
wantErr: false,
},
// delete the templates code end
{
name: "DeleteByID",
fn: func() (interface{}, error) {
// todo test after filling in parameters
return cli.DeleteByID(ctx, &pb.DeleteUserExampleByIDRequest{
Id: 3,
})
},
wantErr: false,
},
{
name: "GetByID",
fn: func() (interface{}, error) {
// todo test after filling in parameters
return cli.GetByID(ctx, &pb.GetUserExampleByIDRequest{
Id: 3,
})
},
wantErr: false,
},
{
name: "List",
fn: func() (interface{}, error) {
// todo test after filling in parameters
return cli.List(ctx, &pb.ListUserExampleRequest{
Params: &types.Params{
Page: 0,
Limit: 10,
Sort: "",
Columns: []*types.Column{
{
Name: "id",
Exp: "<",
Value: "100",
Logic: "",
},
},
},
})
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.fn()
if (err != nil) != tt.wantErr {
t.Errorf("test '%s' error = %v, wantErr %v", tt.name, err, tt.wantErr)
return
}
t.Log("reply data: ", got)
})
}
}
// 压测userExample的各个方法完成后复制报告路径到浏览器查看
func Test_userExampleService_benchmark(t *testing.T) {
err := config.Init(config.Path("conf.yml"))
if err != nil {
panic(err)
}
host := fmt.Sprintf("127.0.0.1:%d", config.Get().Grpc.Port)
protoFile := config.Path("../api/userExample/v1/userExample.proto")
// 如果压测过程中缺少第三方依赖复制到项目的third_party目录下(不包括import路径)
importPaths := []string{
config.Path("../third_party"), // third_party目录
config.Path(".."), // third_party的上一级目录
}
tests := []struct {
name string
fn func() error
wantErr bool
}{
{
name: "GetByID",
fn: func() error {
// todo test after filling in parameters
message := &pb.GetUserExampleByIDRequest{
Id: 3,
}
b, err := benchmark.New(host, protoFile, "GetByID", message, 100, importPaths...)
if err != nil {
return err
}
return b.Run()
},
wantErr: false,
},
{
name: "List",
fn: func() error {
// todo test after filling in parameters
message := &pb.ListUserExampleRequest{
Params: &types.Params{
Page: 0,
Limit: 10,
Sort: "",
Columns: []*types.Column{
{
Name: "age",
Exp: ">=",
Value: "15",
Logic: "",
},
},
},
}
b, err := benchmark.New(host, protoFile, "List", message, 100, importPaths...)
if err != nil {
return err
}
return b.Run()
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.fn()
if (err != nil) != tt.wantErr {
t.Errorf("test '%s' error = %v, wantErr %v", tt.name, err, tt.wantErr)
return
}
})
}
}

80
pkg/app/README.md Normal file
View File

@ -0,0 +1,80 @@
## app
优雅的启动和停止服务,使用[errgroup](golang.org/x/sync/errgroup)保证多个服务同时正常启动。
<br>
### 安装
> go get -u github.com/zhufuyi/pkg/app
<br>
### 使用示例
```go
func main() {
inits := registerInits()
servers := registerServers()
closes := registerCloses(servers)
s := app.New(inits, servers, closes)
s.Run()
}
func registerInits() []app.Init {
// 读取配置文件
var inits []app.Init
// 初始化日志
inits = append(inits, func() {
})
// 初始化数据库
inits = append(inits, func() {
})
// ......
return inits
}
func registerServers() []app.IServer {
var servers []app.IServer
// 创建http服务
servers = append(servers, server.NewHTTPServer(
))
// 创建grpc服务
servers = append(servers, server.NewGRPCServer(
))
// ......
return servers
}
func registerCloses(servers []app.IServer) []app.Close {
var closes []app.Close
// 关闭服务
for _, server := range servers {
closes = append(closes, server.Stop)
}
// 关闭数据库连接
closes = append(closes, func() error {
})
// ......
return closes
}
```

103
pkg/app/app.go Normal file
View File

@ -0,0 +1,103 @@
package app
import (
"context"
"os"
"os/signal"
"syscall"
"github.com/zhufuyi/sponge/pkg/logger"
"golang.org/x/sync/errgroup"
)
// Init app initialization
type Init func()
// Close app close
type Close func() error
// IServer http or grpc server interface
type IServer interface {
Start() error
Stop() error
String() string
}
// App servers
type App struct {
inits []Init
servers []IServer
closes []Close
}
// New create an app
func New(inits []Init, servers []IServer, closes []Close) *App {
for _, init := range inits {
init()
}
return &App{
inits: inits,
servers: servers,
closes: closes,
}
}
// Run servers
func (a *App) Run() {
// ctx will be notified whenever an error occurs in one of the goroutines
eg, ctx := errgroup.WithContext(context.Background())
// start all servers
for _, server := range a.servers {
s := server
eg.Go(func() error {
logger.Infof("start up %s", s.String())
if err := s.Start(); err != nil {
return err
}
return nil
})
}
// watch and stop app
eg.Go(func() error {
return a.watch(ctx)
})
if err := eg.Wait(); err != nil {
panic(err)
}
}
// watch the os signal and the ctx signal from the errgroup, and stop the service if either signal is triggered
func (a *App) watch(ctx context.Context) error {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
for {
select {
case <-ctx.Done(): // service error signals
_ = a.stop()
return ctx.Err()
case s := <-quit: // system notification signal
logger.Infof("receive a quit signal: %s", s.String())
if err := a.stop(); err != nil {
return err
}
logger.Infof("stop app successfully")
return nil
}
}
}
// stopping services and releasing resources
func (a *App) stop() error {
for _, closeFn := range a.closes {
if err := closeFn(); err != nil {
return err
}
}
return nil
}

94
pkg/cache/README.md vendored Normal file
View File

@ -0,0 +1,94 @@
## cache
内存类型的有memory和NoSQL
NoSQL的主要有: redis
各类库只要实现了cache定义的接口(driver)即可。
> 这里的接口driver命名参考了Go官方mysql接口的命名规范
## 多级缓存
### 二级缓存
这里的多级主要是指二级缓存:本地缓存(一级缓存L1)+redis缓存(二级缓存L2)
使用本地缓存可以减少应用服务器到redis之间的网络I/O开销
> 需要注意的是:在并发量不大的系统内,本地缓存的意义不大,反而增加维护的困难。但在高并发系统中,
> 本地缓存可以大大节约带宽。但是要注意本地缓存不是银弹,它会引起多个副本间数据的
> 不一致,还会占据大量的内存,所以不适合保存特别大的数据,而且需要严格考虑刷新机制。
### 过期时间
本地缓存过期时间比分布式缓存小至少一半,以防止本地缓存太久造成多实例数据不一致。
<br>
## 使用示例
```go
// GetByID 根据id获取一条记录
func (d *userExampleDao) GetByID(ctx context.Context, id uint64) (*model.UserExample, error) {
record, err := d.cache.Get(ctx, id)
if errors.Is(err, cacheBase.ErrPlaceholder) {
return nil, model.ErrRecordNotFound
}
// 从mysql获取
if errors.Is(err, goredis.ErrRedisNotFound) {
table := &model.UserExample{}
err := d.db.WithContext(ctx).Where("id = ?", id).First(table).Error
if err != nil {
// if data is empty, set not found cache to prevent cache penetration(防止缓存穿透)
if err.Error() == model.ErrRecordNotFound.Error() {
err = d.cache.SetCacheWithNotFound(ctx, id)
if err != nil {
return nil, err
}
return nil, model.ErrRecordNotFound
}
return nil, err
}
if table.ID == 0 {
return nil, model.ErrRecordNotFound
}
// set cache
err = d.cache.Set(ctx, id, table, cacheBase.DefaultExpireTime)
if err != nil {
return nil, fmt.Errorf("cache.Set error: %v, id=%d", err, id)
}
return table, nil
}
if err != nil {
// fail fast, if cache error return, don't request to db
return nil, err
}
return record, nil
}
```
<br>
## 缓存问题
需要注意以下几个问题:
- 缓存穿透
- 缓存击穿
- 缓存雪崩
可以参考文章:[Redis缓存三大问题](https://mp.weixin.qq.com/s/HjzwefprYSGraU1aJcJ25g)
## Reference
- ristrettohttps://github.com/dgraph-io/ristretto (号称最快的本地缓存)
- [Ristretto简介高性能Go缓存](https://www.yuque.com/kshare/2020/ade1d9b5-5925-426a-9566-3a5587af2181)
- bigcache: https://github.com/allegro/bigcache
- freecache: https://github.com/coocood/freecache
- concurrent_map: https://github.com/easierway/concurrent_map
- gocache: https://github.com/eko/gocache (Built-in stores, eg: bigcache,memcache,redis)

64
pkg/cache/cache.go vendored Normal file
View File

@ -0,0 +1,64 @@
package cache
import (
"context"
"errors"
"time"
)
var (
// DefaultExpireTime 默认过期时间
DefaultExpireTime = time.Hour * 24
// DefaultNotFoundExpireTime 结果为空时的过期时间 1分钟, 常用于数据为空时的缓存时间(缓存穿透)
DefaultNotFoundExpireTime = time.Minute
// NotFoundPlaceholder 占位符
NotFoundPlaceholder = "*"
// DefaultClient 生成一个缓存客户端其中keyPrefix 一般为业务前缀
DefaultClient Cache
// ErrPlaceholder .
ErrPlaceholder = errors.New("cache: placeholder")
// ErrSetMemoryWithNotFound .
ErrSetMemoryWithNotFound = errors.New("cache: set memory cache err for not found")
)
// Cache 定义cache驱动接口
type Cache interface {
Set(ctx context.Context, key string, val interface{}, expiration time.Duration) error
Get(ctx context.Context, key string, val interface{}) error
MultiSet(ctx context.Context, valMap map[string]interface{}, expiration time.Duration) error
MultiGet(ctx context.Context, keys []string, valueMap interface{}) error
Del(ctx context.Context, keys ...string) error
SetCacheWithNotFound(ctx context.Context, key string) error
}
// Set 数据
func Set(ctx context.Context, key string, val interface{}, expiration time.Duration) error {
return DefaultClient.Set(ctx, key, val, expiration)
}
// Get 数据
func Get(ctx context.Context, key string, val interface{}) error {
return DefaultClient.Get(ctx, key, val)
}
// MultiSet 批量set
func MultiSet(ctx context.Context, valMap map[string]interface{}, expiration time.Duration) error {
return DefaultClient.MultiSet(ctx, valMap, expiration)
}
// MultiGet 批量获取
func MultiGet(ctx context.Context, keys []string, valueMap interface{}) error {
return DefaultClient.MultiGet(ctx, keys, valueMap)
}
// Del 批量删除
func Del(ctx context.Context, keys ...string) error {
return DefaultClient.Del(ctx, keys...)
}
// SetCacheWithNotFound .
func SetCacheWithNotFound(ctx context.Context, key string) error {
return DefaultClient.SetCacheWithNotFound(ctx, key)
}

104
pkg/cache/memory.go vendored Normal file
View File

@ -0,0 +1,104 @@
package cache
import (
"context"
"fmt"
"reflect"
"time"
"github.com/zhufuyi/sponge/pkg/encoding"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/dgraph-io/ristretto"
)
type memoryCache struct {
client *ristretto.Cache
KeyPrefix string
encoding encoding.Encoding
}
// NewMemoryCache create a memory cache
func NewMemoryCache(keyPrefix string, encoding encoding.Encoding) Cache {
// see: https://dgraph.io/blog/post/introducing-ristretto-high-perf-go-cache/
// https://www.start.io/blog/we-chose-ristretto-cache-for-go-heres-why/
config := &ristretto.Config{
NumCounters: 1e7, // number of keys to track frequency of (10M).
MaxCost: 1 << 30, // maximum cost of cache (1GB).
BufferItems: 64, // number of keys per Get buffer.
}
store, _ := ristretto.NewCache(config)
return &memoryCache{
client: store,
KeyPrefix: keyPrefix,
encoding: encoding,
}
}
// Set add cache
func (m *memoryCache) Set(ctx context.Context, key string, val interface{}, expiration time.Duration) error {
buf, err := encoding.Marshal(m.encoding, val)
if err != nil {
return fmt.Errorf("encoding.Marshal error: %v, key=%s, val=%+v ", err, key, val)
}
cacheKey, err := BuildCacheKey(m.KeyPrefix, key)
if err != nil {
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
}
m.client.SetWithTTL(cacheKey, buf, 0, expiration)
return nil
}
// Get data
func (m *memoryCache) Get(ctx context.Context, key string, val interface{}) error {
cacheKey, err := BuildCacheKey(m.KeyPrefix, key)
if err != nil {
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
}
data, ok := m.client.Get(cacheKey)
if !ok {
return nil
}
if data == NotFoundPlaceholder {
return ErrPlaceholder
}
err = encoding.Unmarshal(m.encoding, data.([]byte), val)
if err != nil {
return fmt.Errorf("encoding.Unmarshal error: %v, key=%s, cacheKey=%s, type=%v, json=%+v ",
err, key, cacheKey, reflect.TypeOf(val), string(data.([]byte)))
}
return nil
}
// Del 删除
func (m *memoryCache) Del(ctx context.Context, keys ...string) error {
if len(keys) == 0 {
return nil
}
key := keys[0]
cacheKey, err := BuildCacheKey(m.KeyPrefix, key)
if err != nil {
logger.Warn("build cache key error", logger.Err(err), logger.String("key", key))
return err
}
m.client.Del(cacheKey)
return nil
}
// MultiSet 批量set
func (m *memoryCache) MultiSet(ctx context.Context, valMap map[string]interface{}, expiration time.Duration) error {
panic("implement me")
}
// MultiGet 批量获取
func (m *memoryCache) MultiGet(ctx context.Context, keys []string, val interface{}) error {
panic("implement me")
}
func (m *memoryCache) SetCacheWithNotFound(ctx context.Context, key string) error {
if m.client.Set(key, NotFoundPlaceholder, int64(DefaultNotFoundExpireTime)) {
return nil
}
return ErrSetMemoryWithNotFound
}

43
pkg/cache/memory_test.go vendored Normal file
View File

@ -0,0 +1,43 @@
package cache
import (
"context"
"testing"
"github.com/zhufuyi/sponge/pkg/encoding"
"github.com/stretchr/testify/assert"
)
func Test_NewMemoryCache(t *testing.T) {
asserts := assert.New(t)
client := NewMemoryCache("memory-unit-test", encoding.JSONEncoding{})
asserts.NotNil(client)
}
func TestMemoStore_Set(t *testing.T) {
asserts := assert.New(t)
store := NewMemoryCache("memory-unit-test", encoding.JSONEncoding{})
err := store.Set(context.Background(), "test-key", "test-val", -1)
asserts.NoError(err)
}
func TestMemoStore_Get(t *testing.T) {
asserts := assert.New(t)
store := NewMemoryCache("memory-unit-test", encoding.JSONEncoding{})
ctx := context.Background()
// 正常情况
{
var gotVal string
setVal := "test-val"
err := store.Set(ctx, "test-get-key", setVal, 3600)
asserts.NoError(err)
err = store.Get(ctx, "test-get-key", &gotVal)
asserts.NoError(err)
t.Log(setVal, gotVal)
asserts.Equal(setVal, gotVal)
}
}

200
pkg/cache/redis.go vendored Normal file
View File

@ -0,0 +1,200 @@
package cache
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"time"
"github.com/zhufuyi/sponge/pkg/encoding"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/go-redis/redis/v8"
)
// redisCache redis cache结构体
type redisCache struct {
client *redis.Client
KeyPrefix string
encoding encoding.Encoding
DefaultExpireTime time.Duration
newObject func() interface{}
}
// NewRedisCache new一个cache, client 参数是可传入的,方便进行单元测试
func NewRedisCache(client *redis.Client, keyPrefix string, encoding encoding.Encoding, newObject func() interface{}) Cache {
return &redisCache{
client: client,
KeyPrefix: keyPrefix,
encoding: encoding,
newObject: newObject,
}
}
func (c *redisCache) Set(ctx context.Context, key string, val interface{}, expiration time.Duration) error {
buf, err := encoding.Marshal(c.encoding, val)
if err != nil {
return fmt.Errorf("encoding.Marshal error: %v, key=%s, val=%+v ", err, key, val)
}
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
if err != nil {
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
}
if expiration == 0 {
expiration = DefaultExpireTime
}
err = c.client.Set(ctx, cacheKey, buf, expiration).Err()
if err != nil {
return fmt.Errorf("c.client.Set error: %v, cacheKey=%s", err, cacheKey)
}
return nil
}
func (c *redisCache) Get(ctx context.Context, key string, val interface{}) error {
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
if err != nil {
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
}
bytes, err := c.client.Get(ctx, cacheKey).Bytes()
// NOTE: don't handle the case where redis value is nil
// but leave it to the upstream for processing if need
if err != nil {
return err
}
// 防止data为空时Unmarshal报错
if string(bytes) == "" {
return nil
}
if string(bytes) == NotFoundPlaceholder {
return ErrPlaceholder
}
err = encoding.Unmarshal(c.encoding, bytes, val)
if err != nil {
return fmt.Errorf("encoding.Unmarshal error: %v, key=%s, cacheKey=%s, type=%v, json=%+v ",
err, key, cacheKey, reflect.TypeOf(val), string(bytes))
}
return nil
}
func (c *redisCache) MultiSet(ctx context.Context, valueMap map[string]interface{}, expiration time.Duration) error {
if len(valueMap) == 0 {
return nil
}
if expiration == 0 {
expiration = DefaultExpireTime
}
// key-value是成对的所以这里的容量是map的2倍
paris := make([]interface{}, 0, 2*len(valueMap))
for key, value := range valueMap {
buf, err := encoding.Marshal(c.encoding, value)
if err != nil {
logger.Warn("marshal data error", logger.Err(err), logger.Any("value", value))
continue
}
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
if err != nil {
logger.Warn("build cache key error", logger.Err(err), logger.String("key", key))
continue
}
paris = append(paris, []byte(cacheKey))
paris = append(paris, buf)
}
pipeline := c.client.Pipeline()
err := pipeline.MSet(ctx, paris...).Err()
if err != nil {
return fmt.Errorf("pipeline.MSet error: %v", err)
}
for i := 0; i < len(paris); i = i + 2 {
switch paris[i].(type) {
case []byte:
pipeline.Expire(ctx, string(paris[i].([]byte)), expiration)
default:
logger.Warnf("redis expire is unsupported key type: %+v", reflect.TypeOf(paris[i]))
}
}
_, err = pipeline.Exec(ctx)
if err != nil {
return fmt.Errorf("pipeline.Exec error: %v", err)
}
return nil
}
func (c *redisCache) MultiGet(ctx context.Context, keys []string, value interface{}) error {
if len(keys) == 0 {
return nil
}
cacheKeys := make([]string, len(keys))
for index, key := range keys {
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
if err != nil {
return fmt.Errorf("BuildCacheKey error: %v, key=%s", err, key)
}
cacheKeys[index] = cacheKey
}
values, err := c.client.MGet(ctx, cacheKeys...).Result()
if err != nil {
return fmt.Errorf("c.client.MGet error: %v, keys=%+v", err, cacheKeys)
}
// 通过反射注入到map
valueMap := reflect.ValueOf(value)
for i, value := range values {
if value == nil {
continue
}
object := c.newObject()
err = encoding.Unmarshal(c.encoding, []byte(value.(string)), object)
if err != nil {
logger.Warnf("unmarshal data error: %+v, key=%s, cacheKey=%s type=%v", err,
keys[i], cacheKeys[i], reflect.TypeOf(value))
continue
}
valueMap.SetMapIndex(reflect.ValueOf(cacheKeys[i]), reflect.ValueOf(object))
}
return nil
}
func (c *redisCache) Del(ctx context.Context, keys ...string) error {
if len(keys) == 0 {
return nil
}
// 批量构建cacheKey
cacheKeys := make([]string, len(keys))
for index, key := range keys {
cacheKey, err := BuildCacheKey(c.KeyPrefix, key)
if err != nil {
logger.Warnf("build cache key err: %+v, key is %+v", err, key)
continue
}
cacheKeys[index] = cacheKey
}
err := c.client.Del(ctx, cacheKeys...).Err()
if err != nil {
return fmt.Errorf("c.client.Del error: %v, keys=%+v", err, cacheKeys)
}
return nil
}
func (c *redisCache) SetCacheWithNotFound(ctx context.Context, key string) error {
return c.client.Set(ctx, key, NotFoundPlaceholder, DefaultNotFoundExpireTime).Err()
}
// BuildCacheKey 构建一个带有前缀的缓存key
func BuildCacheKey(keyPrefix string, key string) (cacheKey string, err error) {
if key == "" {
return "", errors.New("[cache] key should not be empty")
}
cacheKey = key
if keyPrefix != "" {
cacheKey, err = strings.Join([]string{keyPrefix, key}, ":"), nil
}
return
}

108
pkg/cache/redis_test.go vendored Normal file
View File

@ -0,0 +1,108 @@
package cache
import (
"context"
"fmt"
"reflect"
"testing"
"time"
"github.com/zhufuyi/sponge/pkg/encoding"
"github.com/alicebob/miniredis/v2"
"github.com/go-redis/redis/v8"
)
// InitTestRedis 实例化一个可以用于单元测试的redis
func InitTestRedis() *redis.Client {
var mr, err = miniredis.Run()
if err != nil {
panic(err)
}
// 打开下面命令可以测试链接关闭的情况
// defer mr.Close()
fmt.Println("mini redis addr:", mr.Addr())
return redis.NewClient(&redis.Options{
Addr: mr.Addr(),
})
}
func Test_redisCache_SetGet(t *testing.T) {
// 实例化redis客户端
redisClient := InitTestRedis()
// 实例化redis cache
cache := NewRedisCache(redisClient, "unit-test", encoding.JSONEncoding{}, func() interface{} {
return new(int64)
})
ctx := context.Background()
// test set
type setArgs struct {
key string
value interface{}
expiration time.Duration
}
value := "val-001"
setTests := []struct {
name string
cache Cache
args setArgs
wantErr bool
}{
{
"test redis set",
cache,
setArgs{"key-001", &value, 60 * time.Second},
false,
},
}
for _, tt := range setTests {
t.Run(tt.name, func(t *testing.T) {
c := tt.cache
if err := c.Set(ctx, tt.args.key, tt.args.value, tt.args.expiration); (err != nil) != tt.wantErr {
t.Errorf("Set() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
// test get
type args struct {
key string
}
tests := []struct {
name string
cache Cache
args args
wantVal interface{}
wantErr bool
}{
{
"test redis get",
cache,
args{"key-001"},
"val-001",
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := tt.cache
var gotVal interface{}
err := c.Get(ctx, tt.args.key, &gotVal)
if (err != nil) != tt.wantErr {
t.Errorf("Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Log("gotval", gotVal)
if !reflect.DeepEqual(gotVal, tt.wantVal) {
t.Errorf("Get() gotVal = %v, want %v", gotVal, tt.wantVal)
}
})
}
}

17
pkg/conf/README.md Normal file
View File

@ -0,0 +1,17 @@
## conf
解析yaml、json、toml配置文件到go struct结合[goctl](https://github.com/zhufuyi/goctl)工具自动生成config.go到指定目录例如
> goctl covert yaml --file=test.yaml --tags=json --out=/yourProjectName/config。
<br>
### 安装
> go get -u github.com/zhufuyi/pkg/conf
<br>
### 使用示例
具体示例看[config](internal/config)。

View File

@ -0,0 +1,123 @@
// nolint
// code generated from config file
package config
import "github.com/zhufuyi/sponge/pkg/conf"
type Config = GenerateName
var config *Config
// Init parsing configuration files to struct, including yaml, toml, json, etc.
func Init(configFile string, fs ...func()) error {
config = &Config{}
return conf.Parse(configFile, config, fs...)
}
func Show() {
conf.Show(config)
}
func Get() *Config {
if config == nil {
panic("config is nil")
}
return config
}
type GenerateName struct {
App App `yaml:"app" json:"app"`
Etcd Etcd `yaml:"etcd" json:"etcd"`
Grpc Grpc `yaml:"grpc" json:"grpc"`
HTTP HTTP `yaml:"http" json:"http"`
Jaeger Jaeger `yaml:"jaeger" json:"jaeger"`
Logger Logger `yaml:"logger" json:"logger"`
Metrics Metrics `yaml:"metrics" json:"metrics"`
Mysql Mysql `yaml:"mysql" json:"mysql"`
RateLimiter RateLimiter `yaml:"rateLimiter" json:"rateLimiter"`
Redis Redis `yaml:"redis" json:"redis"`
}
type Redis struct {
Addr string `yaml:"addr" json:"addr"`
DB int `yaml:"dB" json:"dB"`
DialTimeout int `yaml:"dialTimeout" json:"dialTimeout"`
Dsn string `yaml:"dsn" json:"dsn"`
MinIdleConn int `yaml:"minIdleConn" json:"minIdleConn"`
Password string `yaml:"password" json:"password"`
PoolSize int `yaml:"poolSize" json:"poolSize"`
PoolTimeout int `yaml:"poolTimeout" json:"poolTimeout"`
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
}
type Etcd struct {
Addrs []string `yaml:"addrs" json:"addrs"`
}
type Jaeger struct {
AgentHost string `yaml:"agentHost" json:"agentHost"`
AgentPort string `yaml:"agentPort" json:"agentPort"`
SamplingRate float64 `yaml:"samplingRate" json:"samplingRate"`
}
type Mysql struct {
ConnMaxLifetime int `yaml:"connMaxLifetime" json:"connMaxLifetime"`
Dsn string `yaml:"dsn" json:"dsn"`
EnableLog bool `yaml:"enableLog" json:"enableLog"`
MaxIdleConns int `yaml:"maxIdleConns" json:"maxIdleConns"`
MaxOpenConns int `yaml:"maxOpenConns" json:"maxOpenConns"`
SlowThreshold int `yaml:"slowThreshold" json:"slowThreshold"`
}
type RateLimiter struct {
Dimension string `yaml:"dimension" json:"dimension"`
MaxLimit int `yaml:"maxLimit" json:"maxLimit"`
QPSLimit int `yaml:"qpsLimit" json:"qpsLimit"`
}
type App struct {
EnableRegistryDiscovery bool `yaml:"enableRegistryDiscovery" json:"enableRegistryDiscovery"`
EnableLimit bool `yaml:"enableLimit" json:"enableLimit"`
EnableMetrics bool `yaml:"enableMetrics" json:"enableMetrics"`
EnableProfile bool `yaml:"enableProfile" json:"enableProfile"`
EnableTracing bool `yaml:"enableTracing" json:"enableTracing"`
Env string `yaml:"env" json:"env"`
HostIP string `yaml:"hostIP" json:"hostIP"`
Name string `yaml:"name" json:"name"`
Version string `yaml:"version" json:"version"`
}
type LogFileConfig struct {
Filename string `yaml:"filename" json:"filename"`
IsCompression bool `yaml:"isCompression" json:"isCompression"`
MaxAge int `yaml:"maxAge" json:"maxAge"`
MaxBackups int `yaml:"maxBackups" json:"maxBackups"`
MaxSize int `yaml:"maxSize" json:"maxSize"`
}
type Logger struct {
Format string `yaml:"format" json:"format"`
IsSave bool `yaml:"isSave" json:"isSave"`
Level string `yaml:"level" json:"level"`
LogFileConfig LogFileConfig `yaml:"logFileConfig" json:"logFileConfig"`
}
type HTTP struct {
Port int `yaml:"port" json:"port"`
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
ServiceName string `yaml:"serviceName" json:"serviceName"`
}
type Grpc struct {
Port int `yaml:"port" json:"port"`
ReadTimeout int `yaml:"readTimeout" json:"readTimeout"`
WriteTimeout int `yaml:"writeTimeout" json:"writeTimeout"`
ServiceName string `yaml:"serviceName" json:"serviceName"`
}
type Metrics struct {
Port int `yaml:"port" json:"port"`
}

View File

@ -0,0 +1,92 @@
# 使用工具自动生成go struct
# goctl covert yaml --file=conf.yml --tags=json
# app 设置
app:
name: "userExample" # 服务名称
env: "dev" # 运行环境dev:开发环境prod:生产环境pre:预生产环境
version: "v0.0.0" # 版本
hostIP: "127.0.0.1" # 主机ip地址
enableProfile: false # 是否开启性能分析功能true:开启false:关闭
enableMetrics: true # 是否开启指标采集true:开启false:关闭
enableLimit: false # 是否开启限流true:开启false:关闭
enableTracing: false # 是否开启链路跟踪true:开启false:关闭
enableRegistryDiscovery: true # 是否开启注册与发现true:开启false:关闭
# http 设置
http:
port: 8080 # 监听端口
readTimeout: 3 # 读超时,单位(秒)
writeTimeout: 90 # 写超时,单位(秒)如果enableProfile为true需要大于60spprof做profiling的默认值是60s
# grpc 服务设置
grpc:
port: 9090 # 监听端口
readTimeout: 3 # 读超时,单位(秒)
writeTimeout: 3 # 写超时,单位(秒)
# logger 设置
logger:
level: "info" # 输出日志级别 debug, info, warn, error默认是debug
format: "console" # 输出格式console或json默认是console
isSave: false # false:输出到终端true:输出到文件默认是false
logFileConfig: # isSave=true时有效
filename: "out.log" # 文件名称默认值out.log
maxSize: 20 # 最大文件大小(MB)默认值10MB
maxBackups: 50 # 保留旧文件的最大个数默认值100个
maxAge: 15 # 保留旧文件的最大天数默认值30天
isCompression: true # 是否压缩/归档旧文件默认值false
# mysql 设置
mysql:
# dsn格式<user>:<pass>@(127.0.0.1:3306)/<db>?[k=v& ......]
dsn: "root:123456@(192.168.3.37:3306)/account?parseTime=true&loc=Local&charset=utf8,utf8mb4"
enableLog: true # 是否开启打印所有日志
slowThreshold: 0 # 如果大于0只打印时间大于阈值的日志优先级比enableLog高单位(毫秒)
maxIdleConns: 3 #设置空闲连接池中连接的最大数量
maxOpenConns: 50 # 设置打开数据库连接的最大数量
connMaxLifetime: 30 # 设置了连接可复用的最大时间,单位(分钟)
# redis 设置
redis:
# dsn只适合redis6版本以上默认用户为defaulturl格式 [user]:<pass>@]127.0.0.1:6379/[db]
dsn: "default:123456@192.168.3.37:6379"
# 适合各个版本redis
addr: 127.0.0.1:6379
password: "123456"
dB: 0
minIdleConn: 20
dialTimeout: 30 # 链接超时,单位(秒)
readTimeout: 500 # 读超时,单位(毫秒)
writeTimeout: 500 # 写超时,单位(毫秒)
poolSize: 100
poolTimeout: 200 # 连接池超时,单位(秒)
# jaeger配置
jaeger:
agentHost: "192.168.3.37"
agentPort: "6831"
samplingRate: 1.0 # 采样率0~1之间0表示禁止采样大于等于1表示采样所有链路
# limit配置
rateLimiter:
dimension: "path" # 限流维度支持path和ip两种默认是path
qpsLimit: 1000 # 持续每秒允许成功请求数默认是500
maxLimit: 2000 # 瞬时最大允许峰值默认是1000通常大于qpsLimit
# metrics配置
metrics:
port: 9082
# etcd配置
etcd:
addrs: ["192.168.3.37:2379"]

View File

@ -0,0 +1,41 @@
package config
import (
"fmt"
"testing"
"time"
"github.com/zhufuyi/sponge/pkg/conf"
)
func TestParseYAML(t *testing.T) {
err := Init("conf.yml") // 解析yaml文件
if err != nil {
t.Fatal(err)
}
conf.Show(config)
}
// 测试更新配置文件
func TestWatch(t *testing.T) {
fs := []func(){
func() {
fmt.Println("更新字段1")
},
func() {
fmt.Println("更新字段2")
},
}
err := Init("conf.yml", fs...)
if err != nil {
t.Error(err)
return
}
for i := 0; i < 30; i++ {
fmt.Println("port:", Get().App.Env)
time.Sleep(time.Second)
}
}

121
pkg/conf/parse.go Normal file
View File

@ -0,0 +1,121 @@
package conf
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"path"
"path/filepath"
"strings"
"github.com/fsnotify/fsnotify"
"github.com/spf13/viper"
)
// Parse 解析配置文件到struct包括yaml、toml、json等文件如果fs不为空开启监听配置文件变化
func Parse(configFile string, obj interface{}, fs ...func()) error {
confFileAbs, err := filepath.Abs(configFile)
if err != nil {
return err
}
filePathStr, filename := filepath.Split(confFileAbs)
if filePathStr == "" {
filePathStr = "."
}
ext := strings.TrimLeft(path.Ext(filename), ".")
filename = strings.ReplaceAll(filename, "."+ext, "") // 不包括后缀名
viper.AddConfigPath(filePathStr) // 路径
viper.SetConfigName(filename) // 名称
viper.SetConfigType(ext) // 从文件名中获取配置类型
err = viper.ReadInConfig()
if err != nil {
return err
}
err = viper.Unmarshal(obj)
if err != nil {
return err
}
if len(fs) > 0 {
watchConfig(obj, fs...)
}
return nil
}
// 监听配置文件更新
func watchConfig(obj interface{}, fs ...func()) {
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
err := viper.Unmarshal(obj)
if err != nil {
fmt.Println("viper.Unmarshal error: ", err)
} else {
// 更新初始化
for _, f := range fs {
f()
}
}
})
}
// Show 打印配置信息(去掉敏感信息)
func Show(obj interface{}, keywords ...string) {
data, err := json.MarshalIndent(obj, "", " ")
if err != nil {
fmt.Println("json.MarshalIndent error: ", err)
return
}
buf := bufio.NewReader(bytes.NewReader(data))
for {
line, err := buf.ReadString('\n')
if err != nil {
break
}
keywords = append(keywords, `"dsn"`, `"password"`)
fmt.Printf(replacePWD(line, keywords...))
}
}
// 替换密码
func replacePWD(line string, keywords ...string) string {
for _, keyword := range keywords {
if strings.Contains(line, keyword) {
index := strings.Index(line, keyword)
if strings.Contains(line, "@") && strings.Contains(line, ":") {
return replaceDSN(line)
} else {
return fmt.Sprintf("%s: \"******\",\n", line[:index+len(keyword)])
}
}
}
return line
}
// 替换dsn的密码
func replaceDSN(str string) string {
mysqlPWD := []byte(str)
start, end := 0, 0
for k, v := range mysqlPWD {
if v == ':' {
start = k
}
if v == '@' {
end = k
break
}
}
if start >= end {
return str
}
return fmt.Sprintf("%s******%s", mysqlPWD[:start+1], mysqlPWD[end:])
}

52
pkg/discovery/README.md Normal file
View File

@ -0,0 +1,52 @@
## discovery
discovery 服务服务发现,与服务注册[registry](../registry)对应支持etcd、consul、nacos三种方式。
### 使用示例
#### etcd
```go
func getETCDDiscovery(etcdEndpoints []string) registry.Discovery {
cli, err := clientv3.New(clientv3.Config{
Endpoints: etcdEndpoints,
DialTimeout: 10 * time.Second,
DialOptions: []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTransportCredentials(insecure.NewCredentials()),
},
})
if err != nil {
panic(err)
}
return etcd.New(cli)
}
func discoveryExample() {
iDiscovery := getETCDDiscovery([]string{"192.168.1.6:2379"})
// 服务发现的endpoint固定格式discovery:///serviceName
endpoint := "discovery:///" + "user"
// grpc客户端
conn, err := grpccli.DialInsecure(ctx, endpoint,
grpccli.WithUnaryInterceptors(interceptor.UnaryClientLog(logger.Get())),
grpccli.WithDiscovery(discovery),
)
if err != nil {
panic(err)
}
// ......
}
```
<br>
#### consul
<br>
#### nacos

96
pkg/discovery/builder.go Normal file
View File

@ -0,0 +1,96 @@
package discovery
import (
"context"
"errors"
"strings"
"time"
"github.com/zhufuyi/sponge/pkg/registry"
"google.golang.org/grpc/resolver"
)
const name = "discovery"
// Option is builder option.
type Option func(o *builder)
// WithTimeout with timeout option.
func WithTimeout(timeout time.Duration) Option {
return func(b *builder) {
b.timeout = timeout
}
}
// WithInsecure with isSecure option.
func WithInsecure(insecure bool) Option {
return func(b *builder) {
b.insecure = insecure
}
}
// DisableDebugLog disables update instances log.
func DisableDebugLog() Option {
return func(b *builder) {
b.debugLogDisabled = true
}
}
type builder struct {
discoverer registry.Discovery
timeout time.Duration
insecure bool
debugLogDisabled bool
}
// NewBuilder creates a builder which is used to factory registry resolvers.
func NewBuilder(d registry.Discovery, opts ...Option) resolver.Builder {
b := &builder{
discoverer: d,
timeout: time.Second * 10,
insecure: false,
debugLogDisabled: false,
}
for _, o := range opts {
o(b)
}
return b
}
func (b *builder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (resolver.Resolver, error) {
var (
err error
w registry.Watcher
)
done := make(chan struct{}, 1)
ctx, cancel := context.WithCancel(context.Background())
go func() {
w, err = b.discoverer.Watch(ctx, strings.TrimPrefix(target.URL.Path, "/"))
close(done)
}()
select {
case <-done:
case <-time.After(b.timeout):
err = errors.New("discovery create watcher overtime")
}
if err != nil {
cancel()
return nil, err
}
r := &discoveryResolver{
w: w,
cc: cc,
ctx: ctx,
cancel: cancel,
insecure: b.insecure,
debugLogDisabled: b.debugLogDisabled,
}
go r.watch()
return r, nil
}
// Scheme return scheme of discovery
func (*builder) Scheme() string {
return name
}

132
pkg/discovery/resolver.go Normal file
View File

@ -0,0 +1,132 @@
package discovery
import (
"context"
"encoding/json"
"errors"
"net/url"
"strconv"
"time"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/zhufuyi/sponge/pkg/registry"
"google.golang.org/grpc/attributes"
"google.golang.org/grpc/resolver"
)
type discoveryResolver struct {
w registry.Watcher
cc resolver.ClientConn
ctx context.Context
cancel context.CancelFunc
insecure bool
debugLogDisabled bool
}
func (r *discoveryResolver) watch() {
for {
select {
case <-r.ctx.Done():
return
default:
}
ins, err := r.w.Next()
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
logger.Errorf("[resolver] Failed to watch discovery endpoint: %v", err)
time.Sleep(time.Second)
continue
}
r.update(ins)
}
}
func (r *discoveryResolver) update(ins []*registry.ServiceInstance) {
addrs := make([]resolver.Address, 0)
endpoints := make(map[string]struct{})
for _, in := range ins {
endpoint, err := parseEndpoint(in.Endpoints, "grpc", !r.insecure)
if err != nil {
logger.Errorf("[resolver] Failed to parse discovery endpoint: %v", err)
continue
}
if endpoint == "" {
continue
}
// filter redundant endpoints
if _, ok := endpoints[endpoint]; ok {
continue
}
endpoints[endpoint] = struct{}{}
addr := resolver.Address{
ServerName: in.Name,
Attributes: parseAttributes(in.Metadata),
Addr: endpoint,
}
addr.Attributes = addr.Attributes.WithValue("rawServiceInstance", in)
addrs = append(addrs, addr)
}
if len(addrs) == 0 {
logger.Warnf("[resolver] Zero endpoint found,refused to write, instances: %v", ins)
return
}
err := r.cc.UpdateState(resolver.State{Addresses: addrs})
if err != nil {
logger.Errorf("[resolver] failed to update state: %s", err)
}
if !r.debugLogDisabled {
b, _ := json.Marshal(ins)
logger.Infof("[resolver] update instances: %s", b)
}
}
func (r *discoveryResolver) Close() {
r.cancel()
err := r.w.Stop()
if err != nil {
logger.Errorf("[resolver] failed to watch top: %s", err)
}
}
func (r *discoveryResolver) ResolveNow(options resolver.ResolveNowOptions) {}
func parseAttributes(md map[string]string) *attributes.Attributes {
var a *attributes.Attributes
for k, v := range md {
if a == nil {
a = attributes.New(k, v)
} else {
a = a.WithValue(k, v)
}
}
return a
}
// parseEndpoint parses an Endpoint URL.
func parseEndpoint(endpoints []string, scheme string, isSecure bool) (string, error) {
for _, e := range endpoints {
u, err := url.Parse(e)
if err != nil {
return "", err
}
if u.Scheme == scheme && IsSecure(u) == isSecure {
return u.Host, nil
}
}
return "", nil
}
// IsSecure parses isSecure for Endpoint URL.
func IsSecure(u *url.URL) bool {
ok, err := strconv.ParseBool(u.Query().Get("isSecure"))
if err != nil {
return false
}
return ok
}

119
pkg/encoding/encoding.go Normal file
View File

@ -0,0 +1,119 @@
package encoding
import (
"encoding"
"errors"
"reflect"
"strings"
)
var (
// ErrNotAPointer .
ErrNotAPointer = errors.New("v argument must be a pointer")
)
// Codec defines the interface gRPC uses to encode and decode messages. Note
// that implementations of this interface must be thread safe; a Codec's
// methods can be called from concurrent goroutines.
type Codec interface {
// Marshal returns the wire format of v.
Marshal(v interface{}) ([]byte, error)
// Unmarshal parses the wire format into v.
Unmarshal(data []byte, v interface{}) error
// Name returns the name of the Codec implementation. The returned string
// will be used as part of content type in transmission. The result must be
// static; the result cannot change between calls.
Name() string
}
var registeredCodecs = make(map[string]Codec)
// RegisterCodec registers the provided Codec for use with all transport clients and
// servers.
//
// The Codec will be stored and looked up by result of its Name() method, which
// should match the content-subtype of the encoding handled by the Codec. This
// is case-insensitive, and is stored and looked up as lowercase. If the
// result of calling Name() is an empty string, RegisterCodec will panic. See
// Content-Type on
// https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#requests for
// more details.
//
// NOTE: this function must only be called during initialization time (i.e. in
// an init() function), and is not thread-safe. If multiple Compressors are
// registered with the same name, the one registered last will take effect.
func RegisterCodec(codec Codec) {
if codec == nil {
panic("cannot register a nil Codec")
}
if codec.Name() == "" {
panic("cannot register Codec with empty string result for Name()")
}
contentSubtype := strings.ToLower(codec.Name())
registeredCodecs[contentSubtype] = codec
}
// GetCodec gets a registered Codec by content-subtype, or nil if no Codec is
// registered for the content-subtype.
//
// The content-subtype is expected to be lowercase.
func GetCodec(contentSubtype string) Codec {
return registeredCodecs[contentSubtype]
}
// Encoding 编码接口定义
type Encoding interface {
Marshal(v interface{}) ([]byte, error)
Unmarshal(data []byte, v interface{}) error
}
// Marshal encode data
func Marshal(e Encoding, v interface{}) (data []byte, err error) {
if !isPointer(v) {
return data, ErrNotAPointer
}
bm, ok := v.(encoding.BinaryMarshaler)
if ok && e == nil {
data, err = bm.MarshalBinary()
return
}
data, err = e.Marshal(v)
if err == nil {
return
}
if ok {
data, err = bm.MarshalBinary()
}
return
}
// Unmarshal decode data
func Unmarshal(e Encoding, data []byte, v interface{}) (err error) {
if !isPointer(v) {
return ErrNotAPointer
}
bm, ok := v.(encoding.BinaryUnmarshaler)
if ok && e == nil {
err = bm.UnmarshalBinary(data)
return err
}
err = e.Unmarshal(data, v)
if err == nil {
return
}
if ok {
return bm.UnmarshalBinary(data)
}
return
}
func isPointer(data interface{}) bool {
switch reflect.ValueOf(data).Kind() {
case reflect.Ptr, reflect.Interface:
return true
default:
return false
}
}

View File

@ -0,0 +1,56 @@
package encoding
import "testing"
func BenchmarkJsonMarshal(b *testing.B) {
a := make([]int, 0, 400)
for i := 0; i < 400; i++ {
a = append(a, i)
}
jsonEncoding := JSONEncoding{}
for n := 0; n < b.N; n++ {
_, err := jsonEncoding.Marshal(a)
if err != nil {
b.Error(err)
}
}
}
func BenchmarkJsonUnmarshal(b *testing.B) {
a := make([]int, 0, 400)
for i := 0; i < 400; i++ {
a = append(a, i)
}
jsonEncoding := JSONEncoding{}
data, err := jsonEncoding.Marshal(a)
if err != nil {
b.Error(err)
}
var result []int
for n := 0; n < b.N; n++ {
err = jsonEncoding.Unmarshal(data, &result)
if err != nil {
b.Error(err)
}
}
}
func BenchmarkMsgpack(b *testing.B) {
// run the Fib function b.N times
a := make([]int, 400)
for i := 0; i < 400; i++ {
a = append(a, i)
}
msgPackEncoding := MsgPackEncoding{}
data, err := msgPackEncoding.Marshal(a)
if err != nil {
b.Error(err)
}
var result []int
for n := 0; n < b.N; n++ {
err = msgPackEncoding.Unmarshal(data, &result)
if err != nil {
b.Error(err)
}
}
}

View File

@ -0,0 +1 @@
package encoding

View File

@ -0,0 +1,28 @@
package encoding
import (
"bytes"
"encoding/gob"
)
// GobEncoding gob encode
type GobEncoding struct{}
// Marshal gob encode
func (g GobEncoding) Marshal(v interface{}) ([]byte, error) {
var (
buffer bytes.Buffer
)
err := gob.NewEncoder(&buffer).Encode(v)
return buffer.Bytes(), err
}
// Unmarshal gob encode
func (g GobEncoding) Unmarshal(data []byte, value interface{}) error {
err := gob.NewDecoder(bytes.NewReader(data)).Decode(value)
if err != nil {
return err
}
return nil
}

68
pkg/encoding/json/json.go Normal file
View File

@ -0,0 +1,68 @@
package json
import (
"encoding/json"
"reflect"
"github.com/zhufuyi/sponge/pkg/encoding"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
// Name is the name registered for the json codec.
const Name = "json"
var (
// MarshalOptions is a configurable JSON format marshaller.
MarshalOptions = protojson.MarshalOptions{
EmitUnpopulated: true,
}
// UnmarshalOptions is a configurable JSON format parser.
UnmarshalOptions = protojson.UnmarshalOptions{
DiscardUnknown: true,
}
)
func init() {
encoding.RegisterCodec(codec{})
}
// codec is a Codec implementation with json.
type codec struct{}
func (codec) Marshal(v interface{}) ([]byte, error) {
switch m := v.(type) {
case json.Marshaler:
return m.MarshalJSON()
case proto.Message:
return MarshalOptions.Marshal(m)
default:
return json.Marshal(m)
}
}
func (codec) Unmarshal(data []byte, v interface{}) error {
switch m := v.(type) {
case json.Unmarshaler:
return m.UnmarshalJSON(data)
case proto.Message:
return UnmarshalOptions.Unmarshal(data, m)
default:
rv := reflect.ValueOf(v)
for rv := rv; rv.Kind() == reflect.Ptr; {
if rv.IsNil() {
rv.Set(reflect.New(rv.Type().Elem()))
}
rv = rv.Elem()
}
if m, ok := reflect.Indirect(rv).Interface().(proto.Message); ok {
return UnmarshalOptions.Unmarshal(data, m)
}
return json.Unmarshal(data, m)
}
}
func (codec) Name() string {
return Name
}

View File

@ -0,0 +1,127 @@
package encoding
import (
"bytes"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"github.com/golang/snappy"
)
// JSONEncoding json格式
type JSONEncoding struct{}
// Marshal json encode
func (j JSONEncoding) Marshal(v interface{}) ([]byte, error) {
buf, err := json.Marshal(v)
return buf, err
}
// Unmarshal json decode
func (j JSONEncoding) Unmarshal(data []byte, value interface{}) error {
err := json.Unmarshal(data, value)
if err != nil {
return err
}
return nil
}
// JSONGzipEncoding json and gzip
type JSONGzipEncoding struct{}
// Marshal json encode and gzip
func (jz JSONGzipEncoding) Marshal(v interface{}) ([]byte, error) {
buf, err := json.Marshal(v)
if err != nil {
return nil, err
}
// var bufSizeBefore = len(buf)
buf, err = GzipEncode(buf)
// log.Infof("gzip_json_compress_ratio=%d/%d=%.2f", bufSizeBefore, len(buf), float64(bufSizeBefore)/float64(len(buf)))
return buf, err
}
// Unmarshal json encode and gzip
func (jz JSONGzipEncoding) Unmarshal(data []byte, value interface{}) error {
jsonData, err := GzipDecode(data)
if err != nil {
return err
}
err = json.Unmarshal(jsonData, value)
if err != nil {
return err
}
return nil
}
// GzipEncode 编码
func GzipEncode(in []byte) ([]byte, error) {
var (
buffer bytes.Buffer
out []byte
err error
)
writer, err := gzip.NewWriterLevel(&buffer, gzip.BestCompression)
if err != nil {
return nil, err
}
_, err = writer.Write(in)
if err != nil {
err = writer.Close()
if err != nil {
return out, err
}
return out, err
}
err = writer.Close()
if err != nil {
return out, err
}
return buffer.Bytes(), nil
}
// GzipDecode 解码
func GzipDecode(in []byte) ([]byte, error) {
reader, err := gzip.NewReader(bytes.NewReader(in))
if err != nil {
var out []byte
return out, err
}
defer func() {
err = reader.Close()
if err != nil {
fmt.Printf("reader close err: %+v", err)
}
}()
return io.ReadAll(reader)
}
// JSONSnappyEncoding json格式和snappy压缩
type JSONSnappyEncoding struct{}
// Marshal 序列化
func (s JSONSnappyEncoding) Marshal(v interface{}) (data []byte, err error) {
b, err := json.Marshal(v)
if err != nil {
return nil, err
}
d := snappy.Encode(nil, b)
return d, nil
}
// Unmarshal 反序列化
func (s JSONSnappyEncoding) Unmarshal(data []byte, value interface{}) error {
b, err := snappy.Decode(nil, data)
if err != nil {
return err
}
return json.Unmarshal(b, value)
}

View File

@ -0,0 +1,21 @@
package encoding
import "github.com/vmihailenco/msgpack"
// MsgPackEncoding msgpack 格式
type MsgPackEncoding struct{}
// Marshal msgpack encode
func (mp MsgPackEncoding) Marshal(v interface{}) ([]byte, error) {
buf, err := msgpack.Marshal(v)
return buf, err
}
// Unmarshal msgpack decode
func (mp MsgPackEncoding) Unmarshal(data []byte, value interface{}) error {
err := msgpack.Unmarshal(data, value)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,39 @@
package proto
import (
"fmt"
"github.com/zhufuyi/sponge/pkg/encoding"
"google.golang.org/protobuf/proto"
)
// Name is the name registered for the proto compressor.
const Name = "proto"
func init() {
encoding.RegisterCodec(codec{})
}
// codec is a Codec implementation with protobuf. It is the default codec for gRPC.
type codec struct{}
func (codec) Marshal(v interface{}) ([]byte, error) {
vv, ok := v.(proto.Message)
if !ok {
return nil, fmt.Errorf("failed to marshal, message is %T, want proto.Message", v)
}
return proto.Marshal(vv)
}
func (codec) Unmarshal(data []byte, v interface{}) error {
vv, ok := v.(proto.Message)
if !ok {
return fmt.Errorf("failed to unmarshal, message is %T, want proto.Message", v)
}
return proto.Unmarshal(data, vv)
}
func (codec) Name() string {
return Name
}

46
pkg/errcode/README.md Normal file
View File

@ -0,0 +1,46 @@
## errcode
错误码通常包括系统级错误码和服务级错误码一共6位十进制数字组成例如200101
| 10 | 01 | 01 |
| :------ | :------ | :------ |
| 对于http错误码20表示服务级错误(10为系统级错误) | 服务模块代码 | 具体错误代码 |
| 对于grpc错误码40表示服务级错误(30为系统级错误) | 服务模块代码 | 具体错误代码 |
- 错误级别占2位数10(http)和30(grpc)表示系统级错误20(http)和40(grpc)表示服务级错误,通常是由用户非法操作引起的。
- 服务模块占两位数:一个大型系统的服务模块通常不超过两位数,如果超过,说明这个系统该拆分了。
- 错误码占两位数:防止一个模块定制过多的错误码,后期不好维护。
<br>
### 安装
> go get -u github.com/zhufuyi/pkg/errcode
<br>
### 使用示例
### http错误码使用示例
```go
// 定义错误码
var ErrLogin = errcode.NewError(200101, "用户名或密码错误")
// 请求返回
response.Error(c, errcode.LoginErr)
```
<br>
### grpc错误码使用示例
```go
// 定义错误码
var ErrLogin = NewRPCStatus(400101, "用户名或密码错误")
// 返回错误
errcode.ErrLogin.Err()
// 返回附带错误详情信息
errcode.ErrLogin.Err(errcode.Any("err", err))
```

97
pkg/errcode/grpc_error.go Normal file
View File

@ -0,0 +1,97 @@
package errcode
import (
"fmt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// GRPCStatus grpc 状态
type GRPCStatus struct {
status *status.Status
}
var statusCodes = map[codes.Code]string{}
// NewGRPCStatus 新建一个status
func NewGRPCStatus(code codes.Code, msg string) *GRPCStatus {
if v, ok := statusCodes[code]; ok {
panic(fmt.Sprintf("grpc status code = %d already exists, please replace with a new error code, old msg = %s", code, v))
} else {
statusCodes[code] = msg
}
return &GRPCStatus{
status: status.New(code, msg),
}
}
// Detail error details
type Detail struct {
key string
val interface{}
}
// String detail key-value
func (d Detail) String() string {
return fmt.Sprintf("%s: {%v}", d.key, d.val)
}
// Any type
func Any(key string, val interface{}) Detail {
return Detail{
key: key,
val: val,
}
}
// RPCErr rpc error
func RPCErr(g *GRPCStatus, details ...Detail) error {
var dts []string
for _, detail := range details {
dts = append(dts, detail.String())
}
return status.Errorf(g.status.Code(), "%s details = %v", g.status.Message(), dts)
}
// Err return error
func (g *GRPCStatus) Err(details ...Detail) error {
var dts []string
for _, detail := range details {
dts = append(dts, detail.String())
}
if len(dts) == 0 {
return status.Errorf(g.status.Code(), "%s", g.status.Message())
}
return status.Errorf(g.status.Code(), "%s details = %v", g.status.Message(), dts)
}
// ToRPCCode 转换为RPC识别的错误码避免返回Unknown状态码
func ToRPCCode(code int) codes.Code {
var statusCode codes.Code
switch code {
case InternalServerError.code:
statusCode = codes.Internal
case InvalidParams.code:
statusCode = codes.InvalidArgument
case Unauthorized.code:
statusCode = codes.Unauthenticated
case NotFound.code:
statusCode = codes.NotFound
case DeadlineExceeded.code:
statusCode = codes.DeadlineExceeded
case AccessDenied.code:
statusCode = codes.PermissionDenied
case LimitExceed.code:
statusCode = codes.ResourceExhausted
case MethodNotAllowed.code:
statusCode = codes.Unimplemented
default:
statusCode = codes.Unknown
}
return statusCode
}

View File

@ -0,0 +1,20 @@
package errcode
import "google.golang.org/grpc/codes"
// nolint
// rpc服务级别错误码有Status前缀
var (
// StatusUserCreate = NewGRPCStatus(400101, "创建用户失败")
// StatusUserDelete = NewGRPCStatus(400102, "删除用户失败")
// StatusUserUpdate = NewGRPCStatus(400103, "更新用户失败")
// StatusUserGet = NewGRPCStatus(400104, "获取用户失败")
)
// GCode 根据编号生成400000~500000之间的错误码
func GCode(NO int) codes.Code {
if NO > 1000 {
panic("NO must be < 1000")
}
return codes.Code(400000 + NO*100)
}

View File

@ -0,0 +1,21 @@
package errcode
// nolint
// rpc系统级别错误码有status前缀
var (
StatusSuccess = NewGRPCStatus(0, "ok")
StatusInvalidParams = NewGRPCStatus(300001, "参数错误")
StatusUnauthorized = NewGRPCStatus(300002, "认证错误")
StatusInternalServerError = NewGRPCStatus(300003, "服务内部错误")
StatusNotFound = NewGRPCStatus(300004, "资源不存在")
StatusAlreadyExists = NewGRPCStatus(300005, "资源已存在")
StatusTimeout = NewGRPCStatus(300006, "超时")
StatusTooManyRequests = NewGRPCStatus(300007, "请求过多")
StatusForbidden = NewGRPCStatus(300008, "拒绝访问")
StatusLimitExceed = NewGRPCStatus(300009, "访问限制")
StatusDeadlineExceeded = NewGRPCStatus(300010, "已超过最后期限")
StatusAccessDenied = NewGRPCStatus(300011, "拒绝访问")
StatusMethodNotAllowed = NewGRPCStatus(300012, "不允许使用的方法")
)

85
pkg/errcode/http_error.go Normal file
View File

@ -0,0 +1,85 @@
package errcode
import (
"fmt"
"net/http"
)
// Error 错误
type Error struct {
// 错误码
code int
// 错误消息
msg string
// 详细信息
details []string
}
var errCodes = map[int]string{}
// NewError 创建新错误信息
func NewError(code int, msg string) *Error {
if v, ok := errCodes[code]; ok {
panic(fmt.Sprintf("http error code = %d already exists, please replace with a new error code, old msg = %s", code, v))
}
errCodes[code] = msg
return &Error{code: code, msg: msg}
}
// String 打印错误
func (e *Error) Error() string {
return fmt.Sprintf("错误码:%d, 错误信息:%s", e.Code(), e.Msg())
}
// Code 错误码
func (e *Error) Code() int {
return e.code
}
// Msg 错误信息
func (e *Error) Msg() string {
return e.msg
}
// Msgf 附加信息
func (e *Error) Msgf(args []interface{}) string {
return fmt.Sprintf(e.msg, args...)
}
// Details 错误详情
func (e *Error) Details() []string {
return e.details
}
// WithDetails 携带附加错误详情
func (e *Error) WithDetails(details ...string) *Error {
newError := *e
newError.details = []string{}
newError.details = append(newError.details, details...)
return &newError
}
// StatusCode 对应http错误码
func (e *Error) StatusCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK
case InternalServerError.Code():
return http.StatusInternalServerError
case InvalidParams.Code():
return http.StatusBadRequest
case Unauthorized.Code():
return http.StatusUnauthorized
case TooManyRequests.Code(), LimitExceed.Code():
return http.StatusTooManyRequests
case Forbidden.Code():
return http.StatusForbidden
case NotFound.Code():
return http.StatusNotFound
case Timeout.Code():
return http.StatusRequestTimeout
}
return http.StatusInternalServerError
}

View File

@ -0,0 +1,18 @@
package errcode
// nolint
// 服务级别错误码有Err前缀
var (
// ErrUserCreate = NewError(200101, "创建用户失败")
// ErrUserDelete = NewError(200102, "删除用户失败")
// ErrUserUpdate = NewError(200103, "更新用户失败")
// ErrUserGet = NewError(200104, "获取用户失败")
)
// HCode 根据编号生成200000~300000之间的错误码
func HCode(NO int) int {
if NO > 1000 {
panic("NO must be < 1000")
}
return 200000 + NO*100
}

View File

@ -0,0 +1,20 @@
package errcode
// nolint
// http系统级别错误码无Err前缀
var (
Success = NewError(0, "ok")
InvalidParams = NewError(100001, "参数错误")
Unauthorized = NewError(100002, "认证错误")
InternalServerError = NewError(100003, "服务内部错误")
NotFound = NewError(100004, "资源不存在")
AlreadyExists = NewError(100005, "资源已存在")
Timeout = NewError(100006, "超时")
TooManyRequests = NewError(100007, "请求过多")
Forbidden = NewError(100008, "拒绝访问")
LimitExceed = NewError(100009, "访问限制")
DeadlineExceeded = NewError(100010, "已超过最后期限")
AccessDenied = NewError(100011, "拒绝访问")
MethodNotAllowed = NewError(100012, "不允许使用的方法")
)

23
pkg/gin/errcode/README.md Normal file
View File

@ -0,0 +1,23 @@
## errcode
错误码通常包括系统级错误码和服务级错误码一共5位十进制数字组成例如20101
| 2 | 01 | 01 |
| :------ | :------ | :------ |
| 2表示服务级错误(1为系统级错误) | 服务模块代码 | 具体错误代码 |
- 错误级别占一位数1表示系统级错误2表示服务级错误通常是由用户非法操作引起的。
- 服务模块占两位数:一个大型系统的服务模块通常不超过两位数,如果超过,说明这个系统该拆分了。
- 错误码占两位数:防止一个模块定制过多的错误码,后期不好维护。
<br>
## 使用示例
```go
// 定义错误码
ErrLogin = errcode.NewError(20101, "用户名或密码错误")
// 请求返回
response.Error(c, errcode.LoginErr)
```

View File

@ -0,0 +1,85 @@
package errcode
import (
"fmt"
"net/http"
)
// Error 错误
type Error struct {
// 错误码
code int
// 错误消息
msg string
// 详细信息
details []string
}
var codes = map[int]string{}
// NewError 创建新错误信息
func NewError(code int, msg string) *Error {
if _, ok := codes[code]; ok {
panic(fmt.Sprintf("错误码 %d 已经存在,请更换一个", code))
}
codes[code] = msg
return &Error{code: code, msg: msg}
}
// String 打印错误
func (e *Error) Error() string {
return fmt.Sprintf("错误码:%d, 错误信息:%s", e.Code(), e.Msg())
}
// Code 错误码
func (e *Error) Code() int {
return e.code
}
// Msg 错误信息
func (e *Error) Msg() string {
return e.msg
}
// Msgf 附加信息
func (e *Error) Msgf(args []interface{}) string {
return fmt.Sprintf(e.msg, args...)
}
// Details 错误详情
func (e *Error) Details() []string {
return e.details
}
// WithDetails 携带附加错误详情
func (e *Error) WithDetails(details ...string) *Error {
newError := *e
newError.details = []string{}
newError.details = append(newError.details, details...)
return &newError
}
// StatusCode 对应http错误码
func (e *Error) StatusCode() int {
switch e.Code() {
case Success.Code():
return http.StatusOK
case InternalServerError.Code():
return http.StatusInternalServerError
case InvalidParams.Code():
return http.StatusBadRequest
case Unauthorized.Code():
return http.StatusUnauthorized
case TooManyRequests.Code(), LimitExceed.Code():
return http.StatusTooManyRequests
case Forbidden.Code():
return http.StatusForbidden
case NotFound.Code():
return http.StatusNotFound
case Timeout.Code():
return http.StatusRequestTimeout
}
return http.StatusInternalServerError
}

View File

@ -0,0 +1,9 @@
package errcode
// 服务级别错误码有Err前缀
var (
// ErrUserCreate = NewError(20101, "创建用户失败")
// ErrUserDelete = NewError(20102, "删除用户失败")
// ErrUserUpdate = NewError(20103, "更新用户失败")
// ErrUserGet = NewError(20104, "获取用户失败")
)

View File

@ -0,0 +1,15 @@
package errcode
// 系统级别错误码无Err前缀
var (
Success = NewError(0, "ok")
InvalidParams = NewError(10001, "参数错误")
Unauthorized = NewError(10002, "认证错误")
InternalServerError = NewError(10003, "服务内部错误")
NotFound = NewError(10004, "资源不存在")
AlreadyExists = NewError(10005, "资源已存在")
Timeout = NewError(10006, "超时")
TooManyRequests = NewError(10007, "请求过多")
Forbidden = NewError(10008, "拒绝访问")
LimitExceed = NewError(10009, "访问限制")
)

View File

@ -0,0 +1,38 @@
package handlerfunc
import (
"net/http"
"github.com/zhufuyi/sponge/pkg/utils"
"github.com/gin-gonic/gin"
)
// checkHealthResponse check health result
type checkHealthResponse struct {
Status string `json:"status"`
Hostname string `json:"hostname"`
}
// CheckHealth check healthy.
// @Summary check health
// @Description check health
// @Tags system
// @Accept json
// @Produce json
// @Success 200 {object} checkHealthResponse{}
// @Router /health [get]
func CheckHealth(c *gin.Context) {
c.JSON(http.StatusOK, checkHealthResponse{Status: "UP", Hostname: utils.GetHostname()})
}
// Ping ping
// @Summary ping
// @Description ping
// @Tags system
// @Accept json
// @Produce json
// @Router /ping [get]
func Ping(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{})
}

View File

@ -0,0 +1,140 @@
## render
gin中间件插件。
<br>
## 安装
> go get -u github.com/zhufuyi/pkg/gin/middleware
<br>
## 使用示例
### 日志中间件
可以设置打印最大长度、添加请求id字段、忽略打印path、自定义[zap](go.uber.org/zap) log
```go
r := gin.Default()
// 默认打印日志
r.Use(middleware.Logging())
// 自定义打印日志
r.Use(middleware.Logging(
middleware.WithMaxLen(400), // 打印body最大长度超过则忽略
//WithRequestIDFromHeader(), // 支持自定义requestID名称
WithRequestIDFromContext(), // 支持自定义requestID名称
//middleware.WithIgnoreRoutes("/hello"), // 忽略/hello
))
// 自定义zap log
log, _ := logger.Init(logger.WithFormat("json"))
r.Use(middlewareLogging(
middleware.WithLog(log),
))
```
<br>
### 允许跨域请求
```go
r := gin.Default()
r.Use(middleware.Cors())
```
<br>
### qps限流
#### path维度的qps限流
```go
r := gin.Default()
// path, 默认qps=500, burst=1000
r.Use(ratelimiter.QPS())
// path, 自定义qps=50, burst=100
r.Use(ratelimiter.QPS(
ratelimiter.WithQPS(50),
ratelimiter.WithBurst(100),
))
```
#### ip维度的qps限流
```go
// ip, 自定义qps=40, burst=80
r.Use(ratelimiter.QPS(
ratelimiter.WithIP(),
ratelimiter.WithQPS(40),
ratelimiter.WithBurst(80),
))
```
<br>
### jwt鉴权
```go
r := gin.Default()
r.GET("/user/:id", middleware.JWT(), userFun) // 需要鉴权
```
<br>
### 链路跟踪
```go
// 初始化trace
func InitTrace(serviceName string) {
exporter, err := tracer.NewJaegerAgentExporter("192.168.3.37", "6831")
if err != nil {
panic(err)
}
resource := tracer.NewResource(
tracer.WithServiceName(serviceName),
tracer.WithEnvironment("dev"),
tracer.WithServiceVersion("demo"),
)
tracer.Init(exporter, resource) // 默认采集全部
}
func NewRouter(
r := gin.Default()
r.Use(middleware.Tracing("your-service-name"))
// ......
)
// 如果有需要可以在程序创建一个span
func SpanDemo(serviceName string, spanName string, ctx context.Context) {
_, span := otel.Tracer(serviceName).Start(
ctx, spanName,
trace.WithAttributes(attribute.String(spanName, time.Now().String())), // 自定义属性
)
defer span.End()
// ......
}
```
<br>
### 监控指标
```go
r := gin.Default()
r.Use(metrics.Metrics(r,
//metrics.WithMetricsPath("/demo/metrics"), // default is /metrics
metrics.WithIgnoreStatusCodes(http.StatusNotFound), // ignore status codes
//metrics.WithIgnoreRequestMethods(http.MethodHead), // ignore request methods
//metrics.WithIgnoreRequestPaths("/ping", "/health"), // ignore request paths
))
```

View File

@ -0,0 +1,66 @@
package middleware
import (
"github.com/zhufuyi/sponge/pkg/errcode"
"github.com/zhufuyi/sponge/pkg/gin/response"
"github.com/zhufuyi/sponge/pkg/jwt"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/gin-gonic/gin"
)
// Auth 鉴权
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
authorization := c.GetHeader("Authorization")
if len(authorization) < 20 {
logger.Error("authorization is illegal", logger.String("authorization", authorization))
response.Error(c, errcode.Unauthorized)
c.Abort()
return
}
token := authorization[7:] // 去掉Bearer 前缀
claims, err := jwt.VerifyToken(token)
if err != nil {
logger.Error("VerifyToken error", logger.Err(err))
response.Error(c, errcode.Unauthorized)
c.Abort()
return
}
c.Set("uid", claims.UID)
c.Next()
}
}
// AuthAdmin 管理员鉴权
func AuthAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
authorization := c.GetHeader("Authorization")
if len(authorization) < 20 {
logger.Error("authorization is illegal", logger.String("authorization", authorization))
response.Error(c, errcode.Unauthorized)
c.Abort()
return
}
token := authorization[7:] // 去掉Bearer 前缀
claims, err := jwt.VerifyToken(token)
if err != nil {
logger.Error("VerifyToken error", logger.Err(err))
response.Error(c, errcode.Unauthorized)
c.Abort()
return
}
// 判断是否为管理员
if claims.Role != "admin" {
logger.Error("prohibition of access", logger.String("uid", claims.UID), logger.String("role", claims.Role))
response.Error(c, errcode.Forbidden)
c.Abort()
return
}
c.Set("uid", claims.UID)
c.Next()
}
}

View File

@ -0,0 +1,83 @@
package middleware
import (
"fmt"
"io"
"net/http"
"testing"
"github.com/zhufuyi/sponge/pkg/gin/response"
"github.com/zhufuyi/sponge/pkg/gohttp"
"github.com/gin-gonic/gin"
"github.com/zhufuyi/sponge/pkg/jwt"
)
var uid = "123"
func initServer2() {
jwt.Init()
addr := getAddr()
r := gin.Default()
tokenFun := func(c *gin.Context) {
token, _ := jwt.GenerateToken(uid)
fmt.Println("token =", token)
response.Success(c, token)
}
userFun := func(c *gin.Context) {
response.Success(c, "hello "+uid)
}
r.GET("/token", tokenFun)
r.GET("/user/:id", Auth(), userFun) // 需要鉴权
go func() {
err := r.Run(addr)
if err != nil {
panic(err)
}
}()
}
func TestAuth(t *testing.T) {
initServer2()
// 获取token
result := &gohttp.StdResult{}
err := gohttp.Get(result, requestAddr+"/token")
if err != nil {
t.Fatal(err)
}
token := result.Data.(string)
// 使用访问
authorization := fmt.Sprintf("Bearer %s", token)
val, err := getUser(authorization)
if err != nil {
t.Fatal(err)
}
fmt.Println(val)
}
func getUser(authorization string) (string, error) {
client := &http.Client{}
url := requestAddr + "/user/" + uid
reqest, err := http.NewRequest("GET", url, nil)
reqest.Header.Add("Authorization", authorization)
if err != nil {
return "", err
}
response, _ := client.Do(reqest)
defer response.Body.Close()
data, err := io.ReadAll(response.Body)
if err != nil {
return "", err
}
return string(data), nil
}

View File

@ -0,0 +1,22 @@
package middleware
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// Cors 跨域
func Cors() gin.HandlerFunc {
return cors.New(
cors.Config{
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"},
AllowHeaders: []string{"Origin", "Authorization", "Content-Type", "Accept"},
ExposeHeaders: []string{"Content-Length", "text/plain", "Authorization", "Content-Type"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
},
)
}

View File

@ -0,0 +1,203 @@
package middleware
import (
"bytes"
"io"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
var (
// Print body max length
defaultMaxLength = 300
// default zap log
defaultLogger, _ = zap.NewDevelopment()
// Default request id name
defaultRequestIDNameInHeader = "X-Request-Id"
defaultRequestIDNameInContext = "request_id"
// Ignore route list
defaultIgnoreRoutes = map[string]struct{}{
"/ping": {},
"/pong": {},
"/health": {},
}
)
// Option set the gin logger options.
type Option func(*options)
func defaultOptions() *options {
return &options{
maxLength: defaultMaxLength,
log: defaultLogger,
ignoreRoutes: defaultIgnoreRoutes,
requestIDName: "",
requestIDFrom: 0,
}
}
type options struct {
maxLength int
log *zap.Logger
ignoreRoutes map[string]struct{}
requestIDName string
requestIDFrom int // 0: ignore, 1: from header, 2: from context
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithMaxLen logger content max length
func WithMaxLen(maxLen int) Option {
return func(o *options) {
o.maxLength = maxLen
}
}
// WithLog set log
func WithLog(log *zap.Logger) Option {
return func(o *options) {
if log != nil {
o.log = log
}
}
}
// WithIgnoreRoutes no logger content routes
func WithIgnoreRoutes(routes ...string) Option {
return func(o *options) {
for _, route := range routes {
o.ignoreRoutes[route] = struct{}{}
}
}
}
// WithRequestIDFromHeader name is field in header, default value is X-Request-Id
func WithRequestIDFromHeader(name ...string) Option {
var requestIDName string
if len(name) > 0 && name[0] != "" {
requestIDName = name[0]
} else {
requestIDName = defaultRequestIDNameInHeader
}
return func(o *options) {
o.requestIDFrom = 1
o.requestIDName = requestIDName
}
}
// WithRequestIDFromContext name is field in context, default value is request_id
func WithRequestIDFromContext(name ...string) Option {
var requestIDName string
if len(name) > 0 && name[0] != "" {
requestIDName = name[0]
} else {
requestIDName = defaultRequestIDNameInContext
}
return func(o *options) {
o.requestIDFrom = 2
o.requestIDName = requestIDName
}
}
// ------------------------------------------------------------------------------------------
type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
func getBodyData(buf *bytes.Buffer, maxLen int) string {
var body string
if buf.Len() > maxLen {
body = string(buf.Bytes()[:maxLen]) + " ...... "
// 如果有敏感数据需要过滤掉,比如明文密码
} else {
body = buf.String()
}
return body
}
// Logging print request and response info
func Logging(opts ...Option) gin.HandlerFunc {
o := defaultOptions()
o.apply(opts...)
return func(c *gin.Context) {
start := time.Now()
// 忽略打印指定的路由
if _, ok := o.ignoreRoutes[c.Request.URL.Path]; ok {
c.Next()
return
}
// 处理前打印输入信息
buf := bytes.Buffer{}
_, _ = buf.ReadFrom(c.Request.Body)
fields := []zap.Field{
zap.String("method", c.Request.Method),
zap.String("url", c.Request.URL.String()),
}
if c.Request.Method == http.MethodPost || c.Request.Method == http.MethodPut || c.Request.Method == http.MethodPatch || c.Request.Method == http.MethodDelete {
fields = append(fields,
zap.Int("size", buf.Len()),
zap.String("body", getBodyData(&buf, o.maxLength)),
)
}
reqID := ""
if o.requestIDFrom == 1 {
reqID = c.Request.Header.Get(o.requestIDName)
fields = append(fields, zap.String(o.requestIDName, reqID))
} else if o.requestIDFrom == 2 {
if v, isExist := c.Get(o.requestIDName); isExist {
if requestID, ok := v.(string); ok {
reqID = requestID
fields = append(fields, zap.String(o.requestIDName, reqID))
}
}
}
o.log.Info("<<<<", fields...)
c.Request.Body = io.NopCloser(&buf)
// 替换writer
newWriter := &bodyLogWriter{body: &bytes.Buffer{}, ResponseWriter: c.Writer}
c.Writer = newWriter
// 处理请求
c.Next()
// 处理后打印返回信息
fields = []zap.Field{
zap.Int("code", c.Writer.Status()),
zap.String("method", c.Request.Method),
zap.String("url", c.Request.URL.Path),
zap.Int64("time_us", time.Since(start).Nanoseconds()/1000),
zap.Int("size", newWriter.body.Len()),
zap.String("response", strings.TrimRight(getBodyData(newWriter.body, o.maxLength), "\n")),
}
if o.requestIDName != "" {
fields = append(fields, zap.String(o.requestIDName, reqID))
}
o.log.Info(">>>>", fields...)
}
}

View File

@ -0,0 +1,153 @@
package middleware
import (
"fmt"
"net"
"testing"
"github.com/zhufuyi/sponge/pkg/gin/response"
"github.com/zhufuyi/sponge/pkg/gohttp"
"github.com/zhufuyi/sponge/pkg/logger"
"github.com/zhufuyi/sponge/pkg/utils"
"github.com/gin-gonic/gin"
)
var requestAddr string
func initServer1() {
addr := getAddr()
r := gin.Default()
r.Use(RequestID())
// 默认打印日志
// r.Use(Logging())
// 自定义打印日志
r.Use(Logging(
WithMaxLen(400),
//WithRequestIDFromHeader(),
WithRequestIDFromContext(),
//WithIgnoreRoutes("/hello"), // 忽略/hello
))
// 自定义zap log
//log, _ := logger.Init(logger.WithFormat("json"))
//r.Use(Logging(
// WithLog(log),
//))
helloFun := func(c *gin.Context) {
logger.Info("test request id", utils.FieldRequestIDFromContext(c))
response.Success(c, "hello world")
}
r.GET("/hello", helloFun)
r.DELETE("/hello", helloFun)
r.POST("/hello", helloFun)
r.PUT("/hello", helloFun)
r.PATCH("/hello", helloFun)
go func() {
err := r.Run(addr)
if err != nil {
panic(err)
}
}()
}
// ------------------------------------------------------------------------------------------
func TestRequest(t *testing.T) {
initServer1()
wantHello := "hello world"
result := &gohttp.StdResult{}
type User struct {
Name string `json:"name"`
}
t.Run("get hello", func(t *testing.T) {
err := gohttp.Get(result, requestAddr+"/hello", gohttp.KV{"id": "100"})
if err != nil {
t.Error(err)
return
}
got := result.Data.(string)
if got != wantHello {
t.Errorf("got: %s, want: %s", got, wantHello)
}
})
t.Run("delete hello", func(t *testing.T) {
err := gohttp.Delete(result, requestAddr+"/hello", gohttp.KV{"id": "100"})
if err != nil {
t.Error(err)
return
}
got := result.Data.(string)
if got != wantHello {
t.Errorf("got: %s, want: %s", got, wantHello)
}
})
t.Run("post hello", func(t *testing.T) {
err := gohttp.Post(result, requestAddr+"/hello", &User{"foo"})
if err != nil {
t.Error(err)
return
}
got := result.Data.(string)
if got != wantHello {
t.Errorf("got: %s, want: %s", got, wantHello)
}
})
t.Run("put hello", func(t *testing.T) {
err := gohttp.Put(result, requestAddr+"/hello", &User{"foo"})
if err != nil {
t.Error(err)
return
}
got := result.Data.(string)
if got != wantHello {
t.Errorf("got: %s, want: %s", got, wantHello)
}
})
t.Run("patch hello", func(t *testing.T) {
err := gohttp.Patch(result, requestAddr+"/hello", &User{"foo"})
if err != nil {
t.Error(err)
return
}
got := result.Data.(string)
if got != wantHello {
t.Errorf("got: %s, want: %s", got, wantHello)
}
})
}
func getAddr() string {
port, _ := getAvailablePort()
requestAddr = fmt.Sprintf("http://localhost:%d", port)
return fmt.Sprintf(":%d", port)
}
func getAvailablePort() (int, error) {
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
if err != nil {
return 0, err
}
listener, err := net.ListenTCP("tcp", address)
if err != nil {
return 0, err
}
port := listener.Addr().(*net.TCPAddr).Port
err = listener.Close()
return port, err
}

View File

@ -0,0 +1,41 @@
## metrics
gin metrics library, collect five metrics, `uptime`, `http_request_count_total`, `http_request_duration_seconds`, `http_request_size_bytes`, `http_response_size_bytes`.
<br>
### Usage
```go
r := gin.Default()
r.Use(metrics.Metrics(r,
//metrics.WithMetricsPath("/demo/metrics"), // default is /metrics
metrics.WithIgnoreStatusCodes(http.StatusNotFound), // ignore status codes
//metrics.WithIgnoreRequestMethods(http.MethodHead), // ignore request methods
//metrics.WithIgnoreRequestPaths("/ping", "/health"), // ignore request paths
))
```
<br>
### Metrics
Details about exposed Prometheus metrics.
| Name | Type | Exposed Information |
| ---- | ---- | ---------------------|
| gin_uptime | Counter | HTTP service uptime. |
| gin_http_request_count_total | Counter | Total number of HTTP requests made. |
| gin_http_request_duration_seconds | Histogram | HTTP request latencies in seconds. |
| gin_http_request_size_bytes | Summary | HTTP request sizes in bytes. |
| gin_http_response_size_bytes | Summary | HTTP response sizes in bytes. |
<br>
### Grafana charts
import [gin_grafana.json](gin_grafana.json) to your grafana, datasource name is `Prometheus`, change the name of the datasource according to your actual datasource.
![metrics](gin_grafana.jpg)

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 KiB

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,140 @@
package metrics
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
namespace = "gin"
labels = []string{"status", "path", "method"}
uptime = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "uptime",
Help: "HTTP service uptime.",
}, nil,
)
reqCount = prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: namespace,
Name: "http_request_count_total",
Help: "Total number of HTTP requests made.",
}, labels,
)
reqDuration = prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Namespace: namespace,
Name: "http_request_duration_seconds",
Help: "HTTP request latencies in seconds.",
}, labels,
)
reqSizeBytes = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Namespace: namespace,
Name: "http_request_size_bytes",
Help: "HTTP request sizes in bytes.",
}, labels,
)
respSizeBytes = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Namespace: namespace,
Name: "http_response_size_bytes",
Help: "HTTP response sizes in bytes.",
}, labels,
)
)
// init registers the prometheus metrics
func initPrometheus() {
prometheus.MustRegister(uptime, reqCount, reqDuration, reqSizeBytes, respSizeBytes)
go recordUptime()
}
// recordUptime increases service uptime per 30 second.
func recordUptime() {
for range time.Tick(time.Second * 30) {
uptime.WithLabelValues().Inc()
}
}
// calcRequestSize returns the size of request object.
func calcRequestSize(r *http.Request) float64 {
size := 0
if r.URL != nil {
size = len(r.URL.String())
}
size += len(r.Method)
size += len(r.Proto)
for name, values := range r.Header {
size += len(name)
for _, value := range values {
size += len(value)
}
}
size += len(r.Host)
// r.Form and r.MultipartForm are assumed to be included in r.URL.
if r.ContentLength != -1 {
size += int(r.ContentLength)
}
return float64(size)
}
// ------------------------------------------------------------------------------------------
// metricsHandler wrappers the standard http.Handler to gin.HandlerFunc
func metricsHandler() gin.HandlerFunc {
return func(c *gin.Context) {
handler := promhttp.Handler()
handler.ServeHTTP(c.Writer, c.Request)
}
}
// Metrics returns a gin.HandlerFunc for exporting some Web metrics
func Metrics(r *gin.Engine, opts ...Option) gin.HandlerFunc {
o := defaultOptions()
o.apply(opts...)
// init prometheus
initPrometheus()
r.GET(o.metricsPath, metricsHandler())
return func(c *gin.Context) {
start := time.Now()
c.Next()
ok := o.isIgnoreCodeStatus(c.Writer.Status()) ||
o.isIgnorePath(c.Request.URL.Path) ||
o.checkIgnoreMethod(c.Request.Method)
if ok {
return
}
// no response content will return -1
respSize := c.Writer.Size()
if respSize < 0 {
respSize = 0
}
lvs := []string{strconv.Itoa(c.Writer.Status()), c.Request.URL.Path, c.Request.Method}
reqCount.WithLabelValues(lvs...).Inc()
reqDuration.WithLabelValues(lvs...).Observe(time.Since(start).Seconds())
reqSizeBytes.WithLabelValues(lvs...).Observe(calcRequestSize(c.Request))
respSizeBytes.WithLabelValues(lvs...).Observe(float64(respSize))
}
}

View File

@ -0,0 +1,157 @@
package metrics
import (
"fmt"
"io"
"net"
"net/http"
"strings"
"testing"
"github.com/gin-gonic/gin"
)
var requestAddr string
func initGin(r *gin.Engine, metricsFun gin.HandlerFunc) {
addr := getAddr()
r.Use(metricsFun)
r.GET("/hello", func(c *gin.Context) {
c.String(200, "[get] hello")
})
go func() {
err := r.Run(addr)
if err != nil {
panic(err)
}
}()
}
func TestMetricsPath(t *testing.T) {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
metricsFun := Metrics(r,
WithMetricsPath("/test/metrics"),
)
initGin(r, metricsFun)
resp, err := http.Get(requestAddr + "/test/metrics")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("code is %d", resp.StatusCode)
}
}
func TestIgnoreStatusCodes(t *testing.T) {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
metricsFun := Metrics(r,
WithIgnoreStatusCodes(http.StatusNotFound),
)
initGin(r, metricsFun)
_, err := http.Get(requestAddr + "/xxxxxx")
if err != nil {
t.Fatal(err)
}
resp, err := http.Get(requestAddr + "/metrics")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(body), `status="404"`) {
t.Fatal("ignore request status code [404] failed")
}
}
func TestIgnoreRequestPaths(t *testing.T) {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
metricsFun := Metrics(r,
WithIgnoreRequestPaths("/hello"),
)
initGin(r, metricsFun)
_, err := http.Get(requestAddr + "/hello")
if err != nil {
t.Fatal(err)
}
resp, err := http.Get(requestAddr + "/metrics")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(body), `path="/hello"`) {
t.Fatal("ignore request paths [/hello] failed")
}
}
func TestIgnoreRequestMethods(t *testing.T) {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
metricsFun := Metrics(r,
WithIgnoreRequestMethods(http.MethodGet),
)
initGin(r, metricsFun)
_, err := http.Get(requestAddr + "/hello")
if err != nil {
t.Fatal(err)
}
resp, err := http.Get(requestAddr + "/metrics")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(body), `method="GET"`) {
t.Fatal("ignore request method [GET] failed")
}
}
func getAddr() string {
port, _ := getAvailablePort()
requestAddr = fmt.Sprintf("http://localhost:%d", port)
return fmt.Sprintf(":%d", port)
}
func getAvailablePort() (int, error) {
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
if err != nil {
return 0, err
}
listener, err := net.ListenTCP("tcp", address)
if err != nil {
return 0, err
}
port := listener.Addr().(*net.TCPAddr).Port
err = listener.Close()
return port, err
}

View File

@ -0,0 +1,95 @@
package metrics
import (
"strings"
)
// Option set the metrics options.
type Option func(*options)
type options struct {
metricsPath string
ignoreStatusCodes map[int]struct{}
ignoreRequestPaths map[string]struct{}
ignoreRequestMethods map[string]struct{}
}
// defaultOptions default value
func defaultOptions() *options {
return &options{
metricsPath: "/metrics",
ignoreStatusCodes: nil,
ignoreRequestPaths: nil,
ignoreRequestMethods: nil,
}
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithMetricsPath set metrics path
func WithMetricsPath(metricsPath string) Option {
return func(o *options) {
o.metricsPath = metricsPath
}
}
// WithIgnoreStatusCodes ignore status codes
func WithIgnoreStatusCodes(statusCodes ...int) Option {
return func(o *options) {
codeMaps := make(map[int]struct{}, len(statusCodes))
for _, code := range statusCodes {
codeMaps[code] = struct{}{}
}
o.ignoreStatusCodes = codeMaps
}
}
// WithIgnoreRequestPaths ignore request paths
func WithIgnoreRequestPaths(paths ...string) Option {
return func(o *options) {
pathMaps := make(map[string]struct{}, len(paths))
for _, path := range paths {
pathMaps[path] = struct{}{}
}
o.ignoreRequestPaths = pathMaps
}
}
// WithIgnoreRequestMethods ignore request methods
func WithIgnoreRequestMethods(methods ...string) Option {
return func(o *options) {
methodMaps := make(map[string]struct{}, len(methods))
for _, method := range methods {
methodMaps[strings.ToUpper(method)] = struct{}{}
}
o.ignoreRequestMethods = methodMaps
}
}
func (o *options) isIgnoreCodeStatus(statusCode int) bool {
if o.ignoreStatusCodes == nil {
return false
}
_, ok := o.ignoreStatusCodes[statusCode]
return ok
}
func (o *options) isIgnorePath(path string) bool {
if o.ignoreRequestPaths == nil {
return false
}
_, ok := o.ignoreRequestPaths[path]
return ok
}
func (o *options) checkIgnoreMethod(method string) bool {
if o.ignoreRequestMethods == nil {
return false
}
_, ok := o.ignoreRequestMethods[strings.ToUpper(method)]
return ok
}

View File

@ -0,0 +1,34 @@
## ratelimiter
gin `path` or `ip` limit.
<br>
### Install
> go get -u github.com/zhufuyi/pkg/gin/middleware/ratelimiter
<br>
### Usage
```go
r := gin.Default()
// e.g. (1) default path limit, qps=500, burst=1000
// r.Use(QPS())
// e.g. (2) path limit, qps=50, burst=100
r.Use(QPS(
WithPath(),
WithQPS(50),
WithBurst(100),
))
// e.g. (3) ip limit, qps=20, burst=40
// r.Use(QPS(
// WithIP(),
// WithQPS(20),
// WithBurst(40),
// ))
```

View File

@ -0,0 +1,67 @@
package ratelimiter
import (
"golang.org/x/time/rate"
)
var (
// default qps value
defaultQPS rate.Limit = 500
// default the maximum instantaneous request spike allowed, burst >= qps
defaultBurst = 1000
// default is path limit, fault:path limit, true:ip limit
defaultIsIP = false //nolint
)
// Option set the rate limits options.
type Option func(*options)
func defaultOptions() *options {
return &options{
qps: defaultQPS,
burst: defaultBurst,
isIP: false,
}
}
type options struct {
qps rate.Limit
burst int
isIP bool // false: path limit, true: IP limit
}
func (o *options) apply(opts ...Option) {
for _, opt := range opts {
opt(o)
}
}
// WithQPS set the qps value
func WithQPS(qps int) Option {
return func(o *options) {
o.qps = rate.Limit(qps)
}
}
// WithBurst set the burst value, burst >= qps
func WithBurst(burst int) Option {
return func(o *options) {
o.burst = burst
}
}
// WithPath set the path limit mode
func WithPath() Option {
return func(o *options) {
o.isIP = false
}
}
// WithIP set the path limit mode
func WithIP() Option {
return func(o *options) {
o.isIP = true
}
}

View File

@ -0,0 +1,94 @@
package ratelimiter
import (
"net/http"
"sync"
"github.com/gin-gonic/gin"
"golang.org/x/time/rate"
)
var l *Limiter
// Limiter is a controller for the request rate.
type Limiter struct {
qpsLimiter sync.Map
}
// NewLimiter instantiation
func NewLimiter() *Limiter {
return &Limiter{}
}
// GetLimiter get Limiter object, can be updated or query
func GetLimiter() *Limiter {
return l
}
// SetLimiter set limiter parameters
// "limit" indicates the number of token buckets to be added at a rate = value/second (e.g. 10 means 1 token every 100 ms)
// "burst" the maximum instantaneous request spike allowed
func (l *Limiter) SetLimiter(limit rate.Limit, burst int) gin.HandlerFunc {
return func(c *gin.Context) {
path := c.FullPath()
l.qpsLimiter.LoadOrStore(path, rate.NewLimiter(limit, burst))
if !l.allow(path) {
c.JSON(http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests))
return
}
c.Next()
}
}
func (l *Limiter) allow(path string) bool {
if limiter, exist := l.qpsLimiter.Load(path); exist {
if ql, ok := limiter.(*rate.Limiter); ok && !ql.Allow() {
return false
}
}
return true
}
// UpdateQPSLimiter updates the settings for a given path's QPS limiter.
func (l *Limiter) UpdateQPSLimiter(path string, limit rate.Limit, burst int) {
if limiter, exist := l.qpsLimiter.Load(path); exist {
limiter.(*rate.Limiter).SetLimit(limit)
limiter.(*rate.Limiter).SetBurst(burst)
} else {
l.qpsLimiter.Store(path, rate.NewLimiter(limit, burst))
}
}
// GetQPSLimiterStatus returns the status of a given path's QPS limiter.
func (l *Limiter) GetQPSLimiterStatus(path string) (limit rate.Limit, burst int) {
if limiter, exist := l.qpsLimiter.Load(path); exist {
return limiter.(*rate.Limiter).Limit(), limiter.(*rate.Limiter).Burst()
}
return 0, 0
}
// QPS set limit qps parameters
func QPS(opts ...Option) gin.HandlerFunc {
o := defaultOptions()
o.apply(opts...)
l = NewLimiter()
return func(c *gin.Context) {
var path string
if !o.isIP {
path = c.FullPath()
} else {
path = c.ClientIP()
}
l.qpsLimiter.LoadOrStore(path, rate.NewLimiter(o.qps, o.burst))
if !l.allow(path) {
c.Abort()
c.JSON(http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests))
return
}
c.Next()
}
}

View File

@ -0,0 +1,199 @@
package ratelimiter
import (
"errors"
"fmt"
"net"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"
"golang.org/x/time/rate"
"github.com/gin-gonic/gin"
)
var requestAddr string
func init() {
addr := getAddr()
gin.SetMode(gin.ReleaseMode)
r := gin.New()
// e.g. (1) path limit, qps=500, burst=1000
// r.Use(QPS())
// e.g. (2) path limit, qps=50, burst=100
r.Use(QPS(
WithPath(),
WithQPS(50),
WithBurst(100),
))
// e.g. (3) ip limit, qps=20, burst=40
// r.Use(QPS(
// WithIP(),
// WithQPS(20),
// WithBurst(40),
// ))
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, "pong "+c.ClientIP())
})
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, "hello "+c.ClientIP())
})
go func() {
err := r.Run(addr)
if err != nil {
panic(err)
}
}()
}
func TestLimiter_QPS(t *testing.T) {
success, failure := 0, 0
start := time.Now()
for i := 0; i < 1000; i++ {
err := get(requestAddr + "/hello")
if err != nil {
failure++
if failure%10 == 0 {
fmt.Printf("%d %v\n", i, err)
}
} else {
success++
}
time.Sleep(time.Millisecond) // 间隔1毫秒
}
time := time.Now().Sub(start).Seconds()
t.Logf("time=%.3fs, success=%d, failure=%d, qps=%.1f", time, success, failure, float64(success)/time)
}
func TestRateLimiter(t *testing.T) {
var pingSuccess, pingFailures int32
var helloSuccess, helloFailures int32
for j := 0; j < 10; j++ {
wg := &sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if err := get(requestAddr + "/ping"); err != nil {
atomic.AddInt32(&pingFailures, 1)
} else {
atomic.AddInt32(&pingSuccess, 1)
}
}(i)
wg.Add(1)
go func(i int) {
defer wg.Done()
if err := get(requestAddr + "/hello"); err != nil {
atomic.AddInt32(&helloFailures, 1)
} else {
atomic.AddInt32(&helloSuccess, 1)
}
}(i)
}
wg.Wait()
fmt.Printf("%s helloSuccess: %d, helloFailures: %d pingSuccess: %d, pingFailures: %d\n", time.Now().Format(time.RFC3339Nano), helloSuccess, helloFailures, pingSuccess, pingFailures)
time.Sleep(time.Millisecond * 200)
}
}
func TestLimiter_GetQPSLimiterStatus(t *testing.T) {
var pingSuccess, pingFailures int32
for j := 0; j < 10; j++ {
wg := &sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if err := get(requestAddr + "/ping"); err != nil {
atomic.AddInt32(&pingFailures, 1)
} else {
atomic.AddInt32(&pingSuccess, 1)
}
}(i)
}
wg.Wait()
qps, _ := GetLimiter().GetQPSLimiterStatus("/ping")
fmt.Printf("%s pingSuccess: %d, pingFailures: %d limit:%.f\n", time.Now().Format(time.RFC3339Nano), pingSuccess, pingFailures, qps)
time.Sleep(time.Millisecond * 200)
}
}
func TestLimiter_UpdateQPSLimiter(t *testing.T) {
var pingSuccess, pingFailures int32
for j := 0; j < 10; j++ {
wg := &sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(i int) {
defer wg.Done()
if err := get(requestAddr + "/ping"); err != nil {
atomic.AddInt32(&pingFailures, 1)
} else {
atomic.AddInt32(&pingSuccess, 1)
}
}(i)
}
wg.Wait()
limit, burst := GetLimiter().GetQPSLimiterStatus("/ping")
GetLimiter().UpdateQPSLimiter("/ping", limit+rate.Limit(j), burst)
fmt.Printf("%s pingSuccess: %d, pingFailures: %d limit:%.f\n", time.Now().Format(time.RFC3339Nano), pingSuccess, pingFailures, limit)
time.Sleep(time.Millisecond * 200)
}
}
func getAddr() string {
port, _ := getAvailablePort()
requestAddr = fmt.Sprintf("http://localhost:%d", port)
return fmt.Sprintf(":%d", port)
}
func getAvailablePort() (int, error) {
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
if err != nil {
return 0, err
}
listener, err := net.ListenTCP("tcp", address)
if err != nil {
return 0, err
}
port := listener.Addr().(*net.TCPAddr).Port
err = listener.Close()
return port, err
}
func get(url string) error {
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return errors.New(http.StatusText(resp.StatusCode))
}
return nil
}

View File

@ -0,0 +1,52 @@
package middleware
import (
"github.com/zhufuyi/sponge/pkg/krand"
"github.com/gin-gonic/gin"
)
const (
// ContextRequestIDKey context request id for context
ContextRequestIDKey = "request_id"
// HeaderXRequestIDKey http header request ID key
HeaderXRequestIDKey = "X-Request-ID"
)
// RequestID is an interceptor that injects a 'X-Request-ID' into the context and request/response header of each request.
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
// Check for incoming header, use it if exists
requestID := c.Request.Header.Get(HeaderXRequestIDKey)
// Create request id
if requestID == "" {
requestID = krand.String(krand.R_All, 12) // 生成长度为12的随机字符串
c.Request.Header.Set(HeaderXRequestIDKey, requestID)
// Expose it for use in the application
c.Set(ContextRequestIDKey, requestID)
}
// Set X-Request-ID header
c.Writer.Header().Set(HeaderXRequestIDKey, requestID)
c.Next()
}
}
// GetRequestIDFromContext returns 'RequestID' from the given context if present.
func GetRequestIDFromContext(c *gin.Context) string {
if v, ok := c.Get(ContextRequestIDKey); ok {
if requestID, ok := v.(string); ok {
return requestID
}
}
return ""
}
// GetRequestIDFromHeaders returns 'RequestID' from the headers if present.
func GetRequestIDFromHeaders(c *gin.Context) string {
return c.Request.Header.Get(HeaderXRequestIDKey)
}

View File

@ -0,0 +1,100 @@
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
otelcontrib "go.opentelemetry.io/contrib"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
semconv "go.opentelemetry.io/otel/semconv/v1.12.0"
oteltrace "go.opentelemetry.io/otel/trace"
)
const (
tracerKey = "otel-tracer"
tracerName = "otelgin"
)
type traceConfig struct {
TracerProvider oteltrace.TracerProvider
Propagators propagation.TextMapPropagator
}
// TraceOption specifies instrumentation configuration options.
type TraceOption func(*traceConfig)
// WithPropagators specifies propagators to use for extracting
// information from the HTTP requests. If none are specified, global
// ones will be used.
func WithPropagators(propagators propagation.TextMapPropagator) TraceOption {
return func(cfg *traceConfig) {
cfg.Propagators = propagators
}
}
// WithTracerProvider specifies a tracer provider to use for creating a tracer.
// If none is specified, the global provider is used.
func WithTracerProvider(provider oteltrace.TracerProvider) TraceOption {
return func(cfg *traceConfig) {
cfg.TracerProvider = provider
}
}
// Tracing returns interceptor that will trace incoming requests.
// The service parameter should describe the name of the (virtual)
// server handling the request.
func Tracing(serviceName string, opts ...TraceOption) gin.HandlerFunc {
cfg := traceConfig{}
for _, opt := range opts {
opt(&cfg)
}
if cfg.TracerProvider == nil {
cfg.TracerProvider = otel.GetTracerProvider()
}
tracer := cfg.TracerProvider.Tracer(
tracerName,
oteltrace.WithInstrumentationVersion(otelcontrib.SemVersion()),
)
if cfg.Propagators == nil {
cfg.Propagators = otel.GetTextMapPropagator()
}
return func(c *gin.Context) {
c.Set(tracerKey, tracer)
savedCtx := c.Request.Context()
defer func() {
c.Request = c.Request.WithContext(savedCtx)
}()
ctx := cfg.Propagators.Extract(savedCtx, propagation.HeaderCarrier(c.Request.Header))
route := c.FullPath()
opts := []oteltrace.SpanStartOption{
oteltrace.WithAttributes(semconv.NetAttributesFromHTTPRequest("tcp", c.Request)...),
oteltrace.WithAttributes(semconv.EndUserAttributesFromHTTPRequest(c.Request)...),
oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(serviceName, route, c.Request)...),
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
}
spanName := route
if spanName == "" {
spanName = fmt.Sprintf("HTTP %s route not found", c.Request.Method)
}
ctx, span := tracer.Start(ctx, spanName, opts...)
defer span.End()
// pass the span through the request context
c.Request = c.Request.WithContext(ctx)
// serve the request to the next interceptor
c.Next()
status := c.Writer.Status()
attrs := semconv.HTTPAttributesFromHTTPStatusCode(status)
spanStatus, spanMessage := semconv.SpanStatusFromHTTPStatusCode(status)
span.SetAttributes(attrs...)
span.SetStatus(spanStatus, spanMessage)
if len(c.Errors) > 0 {
span.SetAttributes(attribute.String("gin.errors", c.Errors.String()))
}
}
}

View File

@ -0,0 +1,41 @@
## response
封装gin返回json数据插件。
<br>
## 安装
> go get -u github.com/zhufuyi/pkg/gin/response
<br>
## 使用示例
`Output`函数返回兼容http状态码
`Success`和`Error`统一返回状态码200在data.code自定义状态码
所有请求统一返回json
```json
{
"code": 0,
"msg": "",
"data": {}
}
```
```go
// c是*gin.Context
// 返回成功
response.Success(c)
// 返回成功,并返回数据
response.Success(c, gin.H{"users":users})
// 返回失败
response.Error(c, errcode.SendEmailErr)
// 返回失败,并返回数据
response.Error(c, errcode.SendEmailErr, gin.H{"user":user})
```

View File

@ -0,0 +1,108 @@
package response
import (
"encoding/json"
"fmt"
"net/http"
"github.com/zhufuyi/sponge/pkg/errcode"
"github.com/gin-gonic/gin"
)
// Result 输出数据格式
type Result struct {
Code int `json:"code"` // 返回码
Msg string `json:"msg"` // 返回信息说明
Data interface{} `json:"data"` // 返回数据
}
func newResp(code int, msg string, data interface{}) *Result {
resp := &Result{
Code: code,
Msg: msg,
}
// 保证返回时data字段不为nil注意resp.Data=[]interface {}时不为nil经过序列化变成了null
if data == nil {
resp.Data = &struct{}{}
} else {
resp.Data = data
}
return resp
}
var jsonContentType = []string{"application/json; charset=utf-8"}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
func writeJSON(c *gin.Context, code int, res interface{}) {
c.Writer.WriteHeader(code)
writeContentType(c.Writer, jsonContentType)
err := json.NewEncoder(c.Writer).Encode(res)
if err != nil {
fmt.Printf("json encode error, err = %s", err.Error())
}
}
func respJSONWithStatusCode(c *gin.Context, code int, msg string, data ...interface{}) {
var FirstData interface{}
if len(data) > 0 {
FirstData = data[0]
}
resp := newResp(code, msg, FirstData)
writeJSON(c, code, resp)
}
// Output 根据http status code返回json数据
func Output(c *gin.Context, code int, msg ...interface{}) {
switch code {
case http.StatusOK:
respJSONWithStatusCode(c, http.StatusOK, "ok", msg...)
case http.StatusBadRequest:
respJSONWithStatusCode(c, http.StatusBadRequest, errcode.InvalidParams.Msg(), msg...)
case http.StatusUnauthorized:
respJSONWithStatusCode(c, http.StatusUnauthorized, errcode.Unauthorized.Msg(), msg...)
case http.StatusForbidden:
respJSONWithStatusCode(c, http.StatusForbidden, errcode.Forbidden.Msg(), msg...)
case http.StatusNotFound:
respJSONWithStatusCode(c, http.StatusNotFound, errcode.NotFound.Msg(), msg...)
case http.StatusRequestTimeout:
respJSONWithStatusCode(c, http.StatusRequestTimeout, errcode.Timeout.Msg(), msg...)
case http.StatusConflict:
respJSONWithStatusCode(c, http.StatusConflict, errcode.AlreadyExists.Msg(), msg...)
case http.StatusInternalServerError:
respJSONWithStatusCode(c, http.StatusInternalServerError, errcode.InternalServerError.Msg(), msg...)
default:
respJSONWithStatusCode(c, code, http.StatusText(code), msg...)
}
}
// 状态码统一200自定义错误码在data.code
func respJSONWith200(c *gin.Context, code int, msg string, data ...interface{}) {
var FirstData interface{}
if len(data) > 0 {
FirstData = data[0]
}
resp := newResp(code, msg, FirstData)
writeJSON(c, http.StatusOK, resp)
}
// Success 正确
func Success(c *gin.Context, data ...interface{}) {
respJSONWith200(c, 0, "ok", data...)
}
// Error 错误
func Error(c *gin.Context, err *errcode.Error, data ...interface{}) {
respJSONWith200(c, err.Code(), err.Msg(), data...)
}

View File

@ -0,0 +1,270 @@
package response
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"testing"
"github.com/zhufuyi/sponge/pkg/errcode"
"github.com/gin-gonic/gin"
)
var (
requestAddr string
wantCode int
wantData interface{}
wantErrInfo *errcode.Error
)
func init() {
port, _ := getAvailablePort()
requestAddr = fmt.Sprintf("http://localhost:%d", port)
addr := fmt.Sprintf(":%d", port)
r := gin.Default()
r.GET("/hello1", func(c *gin.Context) { Output(c, wantCode, wantData) })
r.GET("/hello2", func(c *gin.Context) { Success(c, wantData) })
r.GET("/hello3", func(c *gin.Context) { Error(c, wantErrInfo) })
go func() {
err := r.Run(addr)
if err != nil {
panic(err)
}
}()
}
// 获取可用端口
func getAvailablePort() (int, error) {
address, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:0", "0.0.0.0"))
if err != nil {
return 0, err
}
listener, err := net.ListenTCP("tcp", address)
if err != nil {
return 0, err
}
port := listener.Addr().(*net.TCPAddr).Port
err = listener.Close()
return port, err
}
func do(method string, url string, body interface{}) ([]byte, error) {
var (
resp *http.Response
err error
contentType = "application/json"
)
v, err := json.Marshal(body)
if err != nil {
return nil, err
}
switch method {
case http.MethodGet:
resp, err = http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
case http.MethodPost:
resp, err = http.Post(url, contentType, bytes.NewReader(v))
if err != nil {
return nil, err
}
defer resp.Body.Close()
case http.MethodDelete, http.MethodPut, http.MethodPatch:
req, err := http.NewRequest(method, url, bytes.NewReader(v))
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", contentType)
resp, err = http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
default:
return nil, fmt.Errorf("%s method not supported", method)
}
return io.ReadAll(resp.Body)
}
func get(url string) ([]byte, error) {
return do(http.MethodGet, url, nil)
}
func delete(url string) ([]byte, error) {
return do(http.MethodDelete, url, nil)
}
func post(url string, body interface{}) ([]byte, error) {
return do(http.MethodPost, url, body)
}
func put(url string, body interface{}) ([]byte, error) {
return do(http.MethodPut, url, body)
}
func patch(url string, body interface{}) ([]byte, error) {
return do(http.MethodPatch, url, body)
}
// ------------------------------------------------------------------------------------------
func TestRespond(t *testing.T) {
type args struct {
url string
code int
data interface{}
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "respond 200",
args: args{
url: requestAddr + "/hello1",
code: http.StatusOK,
data: gin.H{"name": "zhangsan"},
},
wantErr: false,
},
{
name: "respond 400",
args: args{
url: requestAddr + "/hello1",
code: http.StatusBadRequest,
data: nil,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wantCode = tt.args.code
wantData = tt.args.data
data, err := get(tt.args.url)
if (err != nil) != tt.wantErr {
t.Errorf("http.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Logf("%s", data)
var resp = &Result{}
err = json.Unmarshal(data, resp)
if err != nil {
t.Error(err)
return
}
if resp.Code != wantCode {
t.Errorf("%s, got = %v, want %v", tt.name, resp.Code, wantCode)
}
})
}
}
func TestSuccess(t *testing.T) {
type args struct {
url string
code int
data interface{}
ei *errcode.Error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "ok",
args: args{
url: requestAddr + "/hello2",
code: http.StatusOK,
data: gin.H{"name": "zhangsan"},
ei: errcode.Success,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wantData = tt.args.data
wantErrInfo = tt.args.ei
data, err := get(tt.args.url)
if (err != nil) != tt.wantErr {
t.Errorf("http.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Logf("%s", data)
var resp = &Result{}
err = json.Unmarshal(data, resp)
if err != nil {
t.Error(err)
return
}
if resp.Code != wantErrInfo.Code() && resp.Msg != wantErrInfo.Msg() {
t.Errorf("%s, got = %v, want %v", tt.name, resp, wantErrInfo)
}
})
}
}
func TestError(t *testing.T) {
type args struct {
url string
code int
data interface{}
ei *errcode.Error
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "unauthorized",
args: args{
url: requestAddr + "/hello3",
code: http.StatusOK,
data: nil,
ei: errcode.Unauthorized,
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
wantErrInfo = tt.args.ei
data, err := get(tt.args.url)
if (err != nil) != tt.wantErr {
t.Errorf("http.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
t.Logf("%s", data)
var resp = &Result{}
err = json.Unmarshal(data, resp)
if err != nil {
t.Error(err)
return
}
if resp.Code != wantErrInfo.Code() && resp.Msg != wantErrInfo.Msg() {
t.Errorf("%s, got = %v, want %v", tt.name, resp, wantErrInfo)
}
})
}
}

View File

@ -0,0 +1,79 @@
## render
gin请求参数校验。
<br>
## 安装
> go get -u github.com/zhufuyi/pkg/gin/validator
<br>
## 使用示例
```go
package main
import (
"net/http"
"github.com/zhufuyi/sponge/pkg/gin/validator"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
func main() {
r := gin.Default()
binding.Validator = validator.Init()
r.Run(":8080")
}
// 从请求body获取
type createUserRequest struct {
Name string `json:"name" form:"name" binding:"required"`
Password string `json:"password" form:"password" binding:"required"`
Age int `json:"age" form:"age" binding:"gte=0,lte=120"`
Email string `json:"email" form:"email" binding:"email"`
}
func CreateUser(c *gin.Context) {
form := &createUserRequest{}
err := c.ShouldBindJSON(form)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"msg": "ok"})
}
// 从请求url参数获取
type getUserRequest struct {
Page int `json:"page" form:"page" binding:"gte=0"`
Size int `json:"size" form:"size" binding:"gt=0"`
Sort string `json:"sort" form:"sort" binding:"-"`
}
func GetUsers(c *gin.Context) {
form := &getUserRequest{}
err := c.ShouldBindQuery(form)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
return
}
users, err := getUsers(form)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"msg": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"users": users})
}
func getUsers(req *getUserRequest) ([]User,error){}
```

Some files were not shown because too many files have changed in this diff Show More