在前面的课时中,我们演示的 Go 例子基本都是一个简单的 main 函数,运行一小段逻辑代码,并没有涉及引入包外代码和组织 Go 项目内包依赖的方法。为了在编写项目代码时,能够引入其他开发者开源的优秀工具包,因此在进行具体的项目开发之前,我们有必要先介绍下 Go 语言的依赖包管理工具——Go Modules 。
在 Go Modules 被正式推出之前,我们一般是在工作目录下组织 Go 项目的开发代码。工作目录一般由 3 个子目录组成:
1.src,项目的源代码或者外部依赖的源代码以包的形式存放于此,一个目录即一个包;
2.pkg,编译后产生的类库存放于此;
3.bin,编译后产生的可执行文件存放于此。
我们一般通过 GOPATH 环境变量指定 Go 项目的工作目录。GOPATH 默认是与 GOROOT 的值一致(我个人习惯将GOPATH和GOROOT分开),指向 Go 的安装目录,在实际开发中可以根据项目需求指定不同的 GOPATH,从而隔离不同项目之间的开发空间。
Go 在 1.11 之后推出了依赖包管理工具 Go Modules,使得开发者可以在 GOPATH 指定的目录外组织项目代码。使用 Go Modules,Go 项目中无须包含工作目录中固定的 3 个子目录。通过 go mod 命令即可创建一个新的 Module :
go mod init moduleName /也可以不加moduleName
比如,我们在 micro-go-course 目录下创建一个新的 Moudule:
go mod init github.com/longjoy/micro-go-course
// output
go: creating new go.mod: module github.com/longjoy/micro-go-course
后续的输出告诉我们名为 github.com/longjoy/micro-go-course 的 Module 生成成功,在 micro-go-course 目录下会生成一个 go.mod 的文件,内容如下:
module github.com/longjoy/micro-go-course
go 1.14
go.mod 文件生成之后,会被 go toolchain 掌控维护,在我们执行 go run、go build、go get、go mod 等各类命令时自动修改和维护 go.mod 文件中的依赖内容。(就是写进依赖包的信息进go.mod文件)
我们可以通过 Go Modules 引入远程依赖包,如 Git Hub 中开源的 Go 开发工具包。但可能会由于网络环境问题,我们在拉取 GitHub 中的开发依赖包时,有时会失败,在此我推荐使用七牛云搭建的 GOPROXY,可以方便我们在开发中更好地拉取远程依赖包。在项目目录(比如GOPATH下的src目录的目录)下执行以下命令即可配置新的 GOPROXY:
go env -w GOPROXY=https://goproxy.cn,direct
比如我们的项目需要引入 Gorm 依赖连接 My SQL 数据库, 这时可以在 micro-go-course 目录下执行如下的 go get 命令(不行就试试go install,注意后面的版本号@xxx,不熟悉可以看看本专栏中的go基础博文):
go get github.com/jinzhu/gorm
go get 命令将会使用 Git(本质上就是使用git从代码仓库比如github拉取开源的代码项目,只是多了自动编译这些工具包生成归档文件) 等代码工具远程获取代码包,并自动完成编译和安装到 GOPATH/bin(可执行文件) 和 GOPATH/pkg(归档文件) 目录下。命令执行结束后我们会发现 go.mod 文件发生如下改变:
上述 require 关键字为项目引入版本是 v1.9.14 的 gorm 依赖包,该依赖包可以在开发中引入使用。在 go.mod 文件中,还存在 replace 和 exclude 关键字,它们分别用于替换依赖模块和忽略依赖模块。
除了 go mod init,还有 go mod download 和 go mod tidy 两个 Go Modules 常用命令。其中,go mod download 命令可以在我们手动修改 go.mod 文件后,手动更新项目的依赖关系;go mod tidy 与 go mod download 命令类似,但不同的是它会移除掉 go.mod 中没被使用的 require 模块。(建议用go mod tidy)
接下来我们就基于 Go-kit 框架开发一个简单的 User 应用,提供用户注册、登录等 HTTP 接口,项目详细代码我已经放到 GitHub 上了https://github.com/longjoy/micro-go-course,你可以参考下(整个项目的源代码我会放在博文后边)。
在前面的课程中,我们介绍过 Go-kit 是一套强大的微服务开发工具集,用于指导开发人员解决分布式系统开发过程中所遇到的问题,帮助开发人员更专注于业务开发。Go-kit 推荐使用 transport、endpoint 和 service 3 层结构来组织项目,它们的作用分别为:
1.transport 层,指定项目提供服务的方式,比如 HTTP 或者 gRPC 等 。
2.endpoint 层,负责接收请求并返回响应。对于每一个服务接口,endpoint 层都使用一个抽象的 Endpoint 来表示 ,我们可以为每一个 Endpoint 装饰 Go-kit 提供的附加功能,如日志记录、限流、熔断等。
3.service 层,提供具体的业务实现接口,endpoint 层中的 Endpoint 通过调用 service 层的接口方法处理请求。
由图我们可以看到 User 应用的项目结构分别由以下“包”组成:
1.dao 包,提供 MySQL 数据层持久化能力;
2.endpoint 包,负责接收请求,并调用 service 包中的业务接口处理请求后返回响应;
3.redis 包,提供 Redis 数据层操作能力;
4.service 包,提供主要业务实现接口;
5.transport 包,对外暴露项目的服务接口;
6.main,应用主入口。
在具体进行开发之前,建议你使用 go mod 初始化项目,并使用 go get 引入以下依赖包:
go get github.com/go-kit/[email protected] // Go -k it 框架
go get github.com/go-redsync/[email protected] // Redis 分布式锁
go get github.com/go-sql-driver/[email protected] // mysql 驱动
go get github.com/gomodule/[email protected]+incompatible // redis 客户端
go get github.com/gorilla/[email protected] // mux 路由
go get github.com/jinzhu/[email protected] // gorm mysql orm 框架
你也可以直接修改go.mod文件,添加如下内容,然后go mod init:
github.com/go-kit/[email protected] // Go -k it 框架
github.com/go-redsync/[email protected] // Redis 分布式锁
github.com/go-sql-driver/[email protected] // mysql 驱动
github.com/gomodule/[email protected]+incompatible // redis 客户端
github.com/gorilla/[email protected] // mux 路由
github.com/jinzhu/[email protected] // gorm mysql orm 框架
如果出现这些包在go.mod中报错,红色显示,报错unresolve dependency:
一般来说版本问题不大,出问题你可以跟句你的go版本来查看官网,对应包需要的版本
接下来我们就按照 service、endpoint、transport 和 main 的顺序构建整个项目。
service 包中主要提供用户服务的业务接口方法。Go 中可以通过 type 和 interface 关键字定义接口,接口代表了调用方和实现方共同遵守的协议,其内定义一系列将要被实现的函数。在 Go 中,一般使用结构体实现接口,如 service 包中定义的 UserService 接口由 UserServiceImpl 结构体实现:
type UserService interface {
// 登录接口
Login(ctx context.Context, email, password string)(UserInfoDTO, error)
// 注册接口
Register(ctx context.Context, vo RegisterUserVO)(UserInfoDTO, error)
}
type UserInfoDTO struct {
ID int64 json:"id"
Username string json:"username"
Email string json:"email"
}
type UserServiceImpl struct {
userDAO dao.UserDAO
}
func (userService UserServiceImpl) Login(ctx context.Context, email, password string)(UserInfoDTO, error) {
// ...
}
func (userService UserServiceImpl) Register(ctx context.Context, vo RegisterUserVO)(UserInfoDTO, error){
// ...
}
在 Go 中,我们可以为一个函数指定其唯一的接收器,接收器可以为任意类型,具备接收器的函数在 Go 中被称作方法。接收器类似面向对象语言中的 this 或者 self,我们可以在方法内部直接使用和修改接收器中的相关属性。接收器可以分为指针类型和非指针类型(就是调用方法的对象),在方法内部对指针类型的接收器修改将会直接反馈到原接收器,而非指针类型的接收器在方法中被操作的数据为原接收器的值拷贝,对其修改并不会影响到原接收器的数据。(其实就是引用类型和值类型区别)
在具体使用时可以根据需要指定接收器的类型,比如当接收器占用内存较大或者需要对原接收器的属性进行修改时,可以使用指针类型接收器;当接收器占用内存较小,且方法只会读取接收器内的属性时,可以采用非指针类型接收器。在上面 UserService 接口的实现中,我们指定了 UserServiceImpl 接收器类型为指针类型。
Go 中接口属于非侵入式设计,要实现接口仅需满足以下两个条件:
1.接口中所有方法均被实现;
2.接收器添加的方法签名和接口的方法签名完全一致。
在上述代码中,UserServiceImpl 结构体就完全实现了 UserService 接口中定义的方法,因此可以说 UserServiceImpl 结构体实现了 UserService 接口。
在 UserInfoDTO 结构体的定义中,我们还使用了 StructTag 为结构体内的字段添加额外的信息。StructTag 一般由一个或者多个键值对组成,用来表述结构体中字段可携带的额外信息。UserInfoDTO 中 json 键类的 StructTag 说明了该字段在 JSON 序列化时的名称,比如 ID 在序列化时会变为 id。
在 endpoint 包中,我们需要构建 RegisterEndpoint 和 LoginEndpoint,将请求转化为 UserService 接口可以处理的参数,并将处理的结果封装为对应的 response 结构体返回给 transport 包。如下代码所示:
type UserEndpoints struct {
RegisterEndpoint endpoint.Endpoint
LoginEndpoint endpoint.Endpoint
}
type LoginRequest struct {
Email string
Password string
}
type LoginResponse struct {
UserInfo service.UserInfoDTO
}
func MakeLoginEndpoint(userService service.UserService) endpoint.Endpoint {
// ... 解析LoginRequest中的参数传递给 UserService.Login 方法处理并将处理结果封装为 LoginResponse 返回
}
type RegisterRequest struct {
Username string
Email string
Password string
}
type RegisterResponse struct {
UserInfo service.UserInfoDTO
}
func MakeRegisterEndpoint(userService service.UserService) endpoint.Endpoint {
// ... 解析RegisterRequest中的参数传递给 UserService.Register 方法处理并将处理结果封装为 RegisterResponse 返回
}
Endpoint 代表了一个通用的函数原型,负责接收请求,处理请求(使用service层来处理),并返回结果。因为 Endpoint 的函数形式是固定的,所以我们可以在外层给 Endpoint 装饰一些额外的能力,比如熔断、日志、限流、负载均衡等能力,这些能力在 Go-kit 框架中都有相应的 Endpoint 装饰器。
在 transport 包中,我们需要将构建好的 Endpoint 通过 HTTP 或者 RPC 的方式暴露出去。如下代码所示:
func MakeHttpHandler(ctx context.Context, endpoints endpoint.UserEndpoints) http.Handler {
r := mux.NewRouter()
// ... 日志和错误处理相关配置
r.Methods("POST").Path("/register").Handler(kithttp.NewServer(
endpoints.RegisterEndpoint,
decodeRegisterRequest,
encodeJSONResponse,
options...,
))
r.Methods("POST").Path("/login").Handler(kithttp.NewServer(
endpoints.LoginEndpoint,
decodeLoginRequest,
encodeJSONResponse,
options...,
))
return r
}
func decodeRegisterRequest(_ context.Context, r http.Request) (interface{}, error) {
// ... 读取 HTTP 请求体中的注册名、注册邮箱和注册密码,封装为 RegisterRequest 请求体
}
func decodeLoginRequest( context.Context, r http.Request) (interface{}, error) {
// ... 读取 HTTP 请求体中的登录邮箱和密码,封装为 LoginRequest 请求体
}
func encodeJSONResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", "application/json;charset=utf-8")
return json.NewEncoder(w).Encode(response)
}
在上述代码中,我们使用 mux 作为 HTTP 请求的路由和分发器(mux包),相比 Go 中原生态的 HTTP 路由包,mux 的路由代码可读性高、路由规则更清晰。上述代码分别将 RegisterEndpoint 和 LoginEndpoint 暴露到 HTTP 的 /register 和 /login 路径下,并指定对应的解码方法和编码方法。解码方法会将 HTTP 请求体中的请求数据解析封装为 XXXRequest 结构体传给对应的 Endpoint 处理,而编码方法会将 Endpoint 处理返回的 XXXResponse 结构体编码为 HTTP 响应返回客户端。
最后是在 main 函数中依次组建 service、endpoint 和 transport,并启动 Web 服务器,代码如下所示:
func main() {
var (
// 服务监听端口
servicePort = flag.Int("service.port", 10086, "service port"))
flag.Parse()
ctx := context.Background()
errChan := make(chan error)
err := dao.InitMysql("127.0.0.1", "3306", "root", "root", "user")
if err != nil{
log.Fatal(err)
}
err = redis.InitRedis("127.0.0.1","6379", "" )
if err != nil{
log.Fatal(err)
}
userService := service.MakeUserServiceImpl(&dao.UserDAOImpl{})
userEndpoints := &endpoint.UserEndpoints{
endpoint.MakeRegisterEndpoint(userService),
endpoint.MakeLoginEndpoint(userService),
}
r := transport.MakeHttpHandler(ctx, userEndpoints)
go func() {
errChan <- http.ListenAndServe(":" + strconv.Itoa(servicePort), r)
}()
go func() {
// 监控系统信号,等待 ctrl + c 系统信号通知服务关闭
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
errChan <- fmt.Errorf("%s", <-c)
}()
error := <-errChan
log.Println(error)
}
在上述代码中,我们依次构建了 service、endpoint 和 transport,并在 10086 端口启动了 Web 服务器,最后通过监听对应的 ctrl + c 系统信号关闭服务。
通过上述流程,我们就详细介绍完了如何基于 Go-kit 开发一个 Web 项目,在配置好相应的 Go Modules 代理、MySQL 数据库和 Redis 数据库后即可通过 go run 命令启动,启动后可以通过请求相应的 HTTP 接口验证效果,如下 curl 命令例子所示:
// 注册
curl -X POST http://localhost:10086/register -H 'content-type: application/x-www-form-urlencoded' -d 'email=aoho%40mail.com&password=aoho&username=aoho'
// 登录
curl -X POST http://localhost:10086/login -H 'content-type: application/x-www-form-urlencoded' -d 'email=aoho%40mail.com&password=aoho'
@和%40都行
注意你得先安装好mysql和redis,然后准备好user库和user表,表结构:
create table user(ID int,Username varchar(50),Password varchar(50),Email varchar(50),created_at timestamp);
在日常的业务开发中,使用数据库对业务数据进行持久化操作是必不可少的。在前面的 User 服务中,我们使用了 Go 中流行的 gorm ORM 库为服务提供 My SQL 数据库操作能力。gorm 是采用 Go 实现的,几乎全功能的 ORM,通过它,我们可以将数据库中的表结构与 Go 中的结构体进行映射,这样既提升了开发的便利性,也降低了 SQL 注入攻击的可能性。
在使用 gorm 前可以使用 Go Modules 或者 go get 引入相应的依赖 github.com/jinzhu/gorm。
gorm 的使用十分简单,通过 gorm.Open 函数即可建立一个相关数据库连接池,如下代码所示:
package dao
import (
"fmt"
"github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"log"
)
var db gorm.DB
func InitMysql(host, port, user, password, dbName string) (err error) {
db, err = gorm.Open("mysql", fmt.Sprintf("%s:%s@(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", user, password, host, port, dbName))
if err != nil{
log.Println(err)
return
}
db.SingularTable(true)
return
}
这里需要指定数据库地址、端口、用户、密码和数据库名等基本信息。在建立好相应数据库的连接池后,即可通过面向对象的方式操作数据库中的表数据,我们需要首先定义相关的表结构体,如 UserEntity 结构体,它对应数据库中的 user 表:
(注意redis要运行着)
type UserEntity struct {
ID int64
Username string
Password string
Email string
CreatedAt time.Time
}
gorm 同样支持 StructTag,可以使用 StructTag 为结构体中的字段添加相应的表字段限制,如指定映射表字段名称、类型等。gorm 中直接调用 gorm.DB.Create 方法即可插入新的数据,如下例子所示:
func (userDAO UserDAOImpl) Save(user UserEntity) error {
return db.Create(user).Error
}
gorm 提供了丰富的查询方法,基本可以实现所有的复杂查询功能,如下面例子所示的使用 Where 查询语句根据 email 查询用户信息:
func (userDAO UserDAOImpl) SelectByEmail(email string)(*UserEntity, error) {
user := &UserEntity{}
err := db.Where("email = ?", email).First(user).Error
return user, err
}
package main
import (
"context"
"flag"
"fmt"
"github.com/longjoy/micro-go-course/section10/user/dao" //dao之类的包已经在main.go文件同目录下,直接导入该文件也可以其实,属于导如自定义包,可以将这个dao包下的内容拷贝到GOROOT/dao包下,然后直接在项目目录中对dao包进行go build go,然后可以直接"dao"即可,其实就是编译非main文件成为归档文件。
"github.com/longjoy/micro-go-course/section10/user/endpoint"
"github.com/longjoy/micro-go-course/section10/user/redis"
"github.com/longjoy/micro-go-course/section10/user/service"
"github.com/longjoy/micro-go-course/section10/user/transport"
"log"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
)
func main() {
var (
// 服务地址和服务名
servicePort = flag.Int("service.port", 10086, "service port")
//waitTime = flag.Int("wait.time", 10, "wait time")
)
flag.Parse()
time.Sleep(10 * time.Second) // 延时启动,等待 MySQL 和 Redis 准备好
ctx := context.Background()
errChan := make(chan error)
err := dao.InitMysql("127.0.0.1", "3306", "root", "100.Acjq", "user")
if err != nil {
log.Fatal(err)
}
err = redis.InitRedis("127.0.0.1", "6379", "")
if err != nil {
log.Fatal(err)
}
userService := service.MakeUserServiceImpl(&dao.UserDAOImpl{})
userEndpoints := &endpoint.UserEndpoints{
endpoint.MakeRegisterEndpoint(userService),
endpoint.MakeLoginEndpoint(userService),
}
r := transport.MakeHttpHandler(ctx, userEndpoints) //返回http头的对象
go func() {
errChan <- http.ListenAndServe(":"+strconv.Itoa(*servicePort), r)
}()
go func() {
// 监控系统信号,等待 ctrl + c 系统信号通知服务关闭
c := make(chan os.Signal, 1)
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
errChan <- fmt.Errorf("%s", <-c) //打印信息成error类型返回,"%s",<-c
}()
error := <-errChan
log.Println(error) //这里不管级别了,直接Println
}
flag包实现命令行参数的解析,flag.Int,int型的flag,分别有三个参数:name表示命令行的名称,value表示命令行的参数的值,usage表示命令行参数的说明和描述。
定义完flag命令行参数后,调用flag.Parse()来对命令行参数进行解析。
&包名,这时候可以使用该包下的跨包资源,比如大写字母开头的结构体。
http包的ListenAndServe启动一个web服务器监听在一个端口上,为每个请求创建一个go例程,并用一个handler处理这些请求,两个参数,一个是要监听的端口,一个是handler,返回一个非空的错误。
os.signal实现对输入信号的访问,含有两个方法,一个是notify方法,监听收到的信号,一个是stop,用来取消监听。
notify将输入得信号转发到它的第一个参数即通道上,可以含有多个信号参数,即监听多个信号,stop让signal包停止向通道发送信号,通道不会在接受到任何信号。
信号类别:
package dao
import (
"fmt"
_ "github.com/go-sql-driver/mysql"
"github.com/jinzhu/gorm"
"log"
)
var db *gorm.DB //DB是一个含有当前的数据连接的信息,是一个结构体,这里顶一个指针,用于后边接收打开的数据库的信息
func InitMysql(host, port, user, password, dbName string) (err error) {
db, err = gorm.Open("mysql", fmt.Sprintf("%s:%s@(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local", user, password, host, port, dbName)) //第一个参数是连接的数据库类型,第二个参数包括用户名,密码,ip地址,端口号,具体的数据库和编码规则,比如db,err:=gorm.Open("mysql","root:123456@tcp(127.0.0.1:3306)/testdb?charset=utf8")
if err != nil{
log.Println(err)
return
}
db.SingularTable(true) //全局设置表名不可以使用复数形式
return
}
在导入路径前加入下划线表示只执行该库的 init 函数而不对其它导出对象进行真正地导入。因为 Go 语言的数据库驱动都会在 init 函数中注册自己,所以我们只需要进行上述操作即可;否则的话,Go 语言的编译器会提示导入了包却没有使用的错误。
gorm是一个使用Go语言编写的ORM框架。它文档齐全,对开发者友好,支持主流数据库。
Sprintf将占位符传入的变量返回为字符串,不显示在终端。
package dao
import "time"
type UserEntity struct { //创建映射表结构的struct
ID int64
Username string
Password string
Email string
CreatedAt time.Time
}
func (UserEntity) TableName() string {
return "user"
}
type UserDAO interface {
SelectByEmail(email string)(*UserEntity, error)
Save(user *UserEntity) error
}
type UserDAOImpl struct {
}
func (userDAO *UserDAOImpl) SelectByEmail(email string)(*UserEntity, error) {
user := &UserEntity{}
err := db.Where("email = ?", email).First(user).Error //执行完Where和First会返回一个DB类型的结构体指针,DB结构体下有Error成员,First参数是地址值,即指针,将信息写进这个结构体中
return user, err
}
func (userDAO *UserDAOImpl) Save(user *UserEntity) error {
return db.Create(user).Error //执行完Create也会返回一个*DB
}
gorm基本的CURD:Create,Delete,where,model
// 获取第一条记录,按主键排序
db.First(&user)
SELECT * FROM users ORDER BY id LIMIT 1;
// 获取最后一条记录,按主键排序
db.Last(&user)
SELECT * FROM users ORDER BY id DESC LIMIT 1;
// 获取所有记录
db.Find(&users)
SELECT * FROM users;
// 使用主键获取记录
db.First(&user, 10)
SELECT * FROM users WHERE id = 10;
// 获取第一个匹配记录
db.Where("name = ?", "jinzhu").First(&user)
SELECT * FROM users WHERE name = 'jinzhu' limit 1;
// 获取所有匹配记录
db.Where("name = ?", "jinzhu").Find(&users)
SELECT * FROM users WHERE name = 'jinzhu';
// IN
db.Where("name in (?)", []string{"jinzhu", "jinzhu 2"}).Find(&users)
// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// AND
db.Where("name = ? AND age >= ?", "jinzhu", "22").Find(&users)
当使用struct查询时,GORM将只查询那些具有值的字段
// Struct
db.Where(&User{Name: "zhangyang", Age: 20}).First(&user)
SELECT * FROM users WHERE name = "zhangyang" AND age = 20 LIMIT 1;
// Map
db.Where(map[string]interface{}{"name": "zhangyang", "age": 20}).Find(&users)
SELECT * FROM users WHERE name = "zhangyang" AND age = 20;
// 主键的Slice
db.Where([]int64{20, 21, 22}).Find(&users)
SELECT * FROM users WHERE id IN (20, 21, 22);
Not条件查询
db.Not("name", "jinzhu").First(&user)
SELECT * FROM users WHERE name <> "jinzhu" LIMIT 1;
// Not In
db.Not("name", []string{"jinzhu", "jinzhu 2"}).Find(&users)
SELECT * FROM users WHERE name NOT IN ("jinzhu", "jinzhu 2");
// Not In slice of primary keys
db.Not([]int64{1,2,3}).First(&user)
SELECT * FROM users WHERE id NOT IN (1,2,3);
db.Not([]int64{}).First(&user)
SELECT * FROM users;
1
2
3
4
5
6
7
8
9
10
11
12
13
Or条件查询
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)
SELECT * FROM users WHERE role = 'admin' OR role = 'super_admin';
// Struct
db.Where("name = 'jinzhu'").Or(User{Name: "jinzhu 2"}).Find(&users)
SELECT * FROM users WHERE name = 'jinzhu' OR name = 'jinzhu 2';
1
2
3
4
5
6
Select 指定要从数据库检索的字段,默认情况下,将选择所有字段;
db.Select("name, age").Find(&users)
SELECT name, age FROM users;
db.Select([]string{"name", "age"}).Find(&users)
SELECT name, age FROM users;
更多复杂的CURD操作语法https://blog.csdn.net/weixin_45604257/article/details/105139862
package endpoint
import (
"context"
"github.com/go-kit/kit/endpoint"
"github.com/longjoy/micro-go-course/section08/user/service"
)
type UserEndpoints struct {
RegisterEndpoint endpoint.Endpoint
LoginEndpoint endpoint.Endpoint
}
type LoginRequest struct {
Email string
Password string
}
type LoginResponse struct {
UserInfo *service.UserInfoDTO `json:"user_info"`
}
func MakeLoginEndpoint(userService service.UserService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) { //给外层函数返回一个函数
req := request.(*LoginRequest) //对一个空接口进行接口断言
userInfo, err := userService.Login(ctx, req.Email, req.Password)
return &LoginResponse{UserInfo:userInfo}, err
}
}
type RegisterRequest struct {
Username string
Email string
Password string
}
type RegisterResponse struct {
UserInfo *service.UserInfoDTO `json:"user_info"` //该成员变量的类型其实是结构体指针
}
func MakeRegisterEndpoint(userService service.UserService) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(*RegisterRequest)
userInfo, err := userService.Register(ctx, &service.RegisterUserVO{
Username:req.Username,
Password:req.Password,
Email:req.Email,
})
return &RegisterResponse{UserInfo:userInfo}, err /&直接访问这个结构体,同时给这个结构体赋值
}
}
type Endpoint func(ctx context.Context, request interface{}) (response interface{}, err error)
Endpoint是一个函数的别名,返回值有两个
package redis
import (
"fmt"
"github.com/go-redsync/redsync"
"github.com/gomodule/redigo/redis"
"time"
)
var pool *redis.Pool //定义一个全局的pool
var redisLock *redsync.Redsync //redsync是reids提供给go的分布式锁的实现
func InitRedis(host, port, password string) error {
pool = &redis.Pool{
MaxIdle: 20,
IdleTimeout: 240 * time.Second,
MaxActive: 50,
Dial: func() (redis.Conn, error) {
c, err := redis.Dial("tcp", fmt.Sprintf("%s:%s", host, port))
if err != nil {
return nil, err
}
if password != "" {
if _, err := c.Do("AUTH", password); err != nil {
c.Close() //关闭redis,使用redis往往会有多个redis服务开启,所以部署redis时留意下配置文件的分配
return nil, err
}
}
return c, err
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
_, err := c.Do("PING")
return err
},
}
redisLock = redsync.New([]redsync.Pool{pool})
return nil
}
func GetRedisConn() (redis.Conn, error) {
conn := pool.Get()
return conn, conn.Err()
}
func GetRedisLock(key string, expireTime time.Duration) *redsync.Mutex {
return redisLock.NewMutex(key, redsync.SetExpiry(expireTime))
}
Redsync提供了一种使用多个Redis连接池创建分布式互斥体的简单方法。
Dial:拨号连接到给定网络上的Redis服务器,使用指定选项的地址。
Do:向服务器发送命令并返回收到的回复。
Conn:表示到Redis服务器的连接。
New:从给定的Redis连接池创建并返回一个新的Redsync实例。(redis锁)
Get:应用程序必须关闭返回的连接。/此方法始终返回有效连接,以便应用程序可以延迟,首次使用连接时的错误处理。如果有错误获取基础连接,然后连接Err、Do、Send、Flush和Receive方法返回该错误。
Duration:表示两个瞬间之间经过的时间作为int64纳秒计数。表示限制了可代表的最大持续时间约为290年。
Mutex:互斥,分布式锁
package service
import (
"context"
"errors"
"github.com/jinzhu/gorm"
"github.com/longjoy/micro-go-course/section08/user/dao"
"github.com/longjoy/micro-go-course/section08/user/redis"
"log"
"time"
)
type UserInfoDTO struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
type RegisterUserVO struct {
Username string
Password string
Email string
}
var (
ErrUserExisted = errors.New("user is existed")
ErrPassword = errors.New("email and password are not match")
ErrRegistering = errors.New("email is registering")
)
type UserService interface {
// 登录接口
Login(ctx context.Context, email, password string) (*UserInfoDTO, error)
// 注册接口
Register(ctx context.Context, vo *RegisterUserVO) (*UserInfoDTO, error)
}
type UserServiceImpl struct {
userDAO dao.UserDAO
}
func MakeUserServiceImpl(userDAO dao.UserDAO) UserService {
return &UserServiceImpl{
userDAO: userDAO,
}
}
func (userService *UserServiceImpl) Login(ctx context.Context, email, password string) (*UserInfoDTO, error) {
user, err := userService.userDAO.SelectByEmail(email)
if err == nil {
if user.Password == password {
return &UserInfoDTO{
ID: user.ID,
Username: user.Username,
Email: user.Email,
}, nil
} else {
return nil, ErrPassword
}
} else {
log.Printf("err : %s", err)
}
return nil, err
}
func (userService UserServiceImpl) Register(ctx context.Context, vo *RegisterUserVO) (*UserInfoDTO, error) {
lock := redis.GetRedisLock(vo.Email, time.Duration(5)*time.Second)
err := lock.Lock() //上锁
if err != nil {
log.Printf("err : %s", err)
return nil, ErrRegistering
}
defer lock.Unlock()
existUser, err := userService.userDAO.SelectByEmail(vo.Email)
if (err == nil && existUser == nil) || err == gorm.ErrRecordNotFound {
newUser := &dao.UserEntity{
Username: vo.Username,
Password: vo.Password,
Email: vo.Email,
}
err = userService.userDAO.Save(newUser)
if err == nil {
return &UserInfoDTO{
ID: newUser.ID,
Username: newUser.Username,
Email: newUser.Email,
}, nil
}
}
if err == nil {
err = ErrUserExisted
}
return nil, err
}
SecondrrRecordNotFound:返回“记录未找到错误”。仅在尝试使用结构查询数据库时发生;使用切片进行查询不会返回此错误
package service
import (
"context"
"github.com/longjoy/micro-go-course/section08/user/dao"
"github.com/longjoy/micro-go-course/section08/user/redis"
"testing"
)
func TestUserServiceImpl_Login(t *testing.T) {
err := dao.InitMysql("127.0.0.1", "3306", "root", "xuan", "user")
if err != nil{
t.Error(err)
t.FailNow()
}
err = redis.InitRedis("127.0.0.1","6379", "" )
if err != nil{
t.Error(err)
t.FailNow()
}
userService := &UserServiceImpl{
userDAO: &dao.UserDAOImpl{},
}
user, err := userService.Login(context.Background(), "[email protected]", "aoho")
if err != nil{
t.Error(err)
t.FailNow()
}
t.Logf("user id is %d", user.ID)
}
func TestUserServiceImpl_Register(t *testing.T) {
err := dao.InitMysql("127.0.0.1", "3306", "root", "xuan", "user")
if err != nil{
t.Error(err)
t.FailNow()
}
err = redis.InitRedis("127.0.0.1","6379", "" )
if err != nil{
t.Error(err)
t.FailNow()
}
userService := &UserServiceImpl{
userDAO: &dao.UserDAOImpl{},
}
user, err := userService.Register(context.Background(),
&RegisterUserVO{
Username:"aoho",
Password:"aoho",
Email:"[email protected]",
})
if err != nil{
t.Error(err)
t.FailNow()
}
t.Logf("user id is %d", user.ID)
}
单元测试:
在testing包中包含一下结构体:
testing.T: 这就是我们平常使用的单元测试
testing.F: 模糊测试, 可以自动生成测试用例
testing.B: 基准测试. 对函数的运行时间进行统计.
testing.M: 测试的钩子函数, 可预置测试前后的操作.
testing.PB: 测试时并行执行.
package transport
import (
"context"
"encoding/json"
"errors"
"github.com/go-kit/kit/log"
"github.com/go-kit/kit/transport"
kithttp "github.com/go-kit/kit/transport/http" //用kit的http包
"github.com/gorilla/mux"
"github.com/longjoy/micro-go-course/section08/user/endpoint"
"net/http"
"os"
)
var (
ErrorBadRequest = errors.New("invalid request parameter")
)
// MakeHttpHandler make http handler use mux
func MakeHttpHandler(ctx context.Context, endpoints *endpoint.UserEndpoints) http.Handler {
r := mux.NewRouter()
kitLog := log.NewLogfmtLogger(os.Stderr) //标准错误输出
kitLog = log.With(kitLog, "ts", log.DefaultTimestampUTC)
kitLog = log.With(kitLog, "caller", log.DefaultCaller)
options := []kithttp.ServerOption{
kithttp.ServerErrorHandler(transport.NewLogErrorHandler(kitLog)),
kithttp.ServerErrorEncoder(encodeError),
}
r.Methods("POST").Path("/register").Handler(kithttp.NewServer(
endpoints.RegisterEndpoint,
decodeRegisterRequest,
encodeJSONResponse,
options...,
))
r.Methods("POST").Path("/login").Handler(kithttp.NewServer(
endpoints.LoginEndpoint,
decodeLoginRequest,
encodeJSONResponse,
options...,
))
return r
}
func decodeRegisterRequest(_ context.Context, r *http.Request) (interface{}, error) {
username := r.FormValue("username")
password := r.FormValue("password")
email := r.FormValue("email")
if username == "" || password == "" || email == ""{
return nil, ErrorBadRequest
}
return &endpoint.RegisterRequest{
Username:username,
Password:password,
Email:email,
},nil
}
func decodeLoginRequest(_ context.Context, r *http.Request) (interface{}, error) {
email := r.FormValue("email")
password := r.FormValue("password")
if email == "" || password == "" {
return nil, ErrorBadRequest
}
return &endpoint.LoginRequest{
Email:email,
Password:password,
},nil
}
func encodeJSONResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", "application/json;charset=utf-8")
return json.NewEncoder(w).Encode(response)
}
func encodeError(_ context.Context, err error, w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
switch err {
default:
w.WriteHeader(http.StatusInternalServerError)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"error": err.Error(),
})
}
gorilla/mux是 gorilla Web 开发工具包中的路由管理库。gorilla Web 开发包是 Go 语言中辅助开发 Web 服务器的工具包。它包括 Web 服务器开发的各个方面,有表单数据处理包gorilla/schema,有 websocket 通信包gorilla/websocket,有各种中间件的包gorilla/handlers,有 session 管理包gorilla/sessions,有安全的 cookie 包gorilla/securecookie。
在我们的项目中,并不是所有的路由都需要通过认证后才可以访问,就比如登录,注册之类的页面,用户是不需要登录即可访问的。
当我们在脱离使用框架后,我们要做的,将不仅仅是让路由访问成功,我们需要做的更多。我们需要将要认证才可以访问的路由,以及不需要认证也可以访问的路由区分开。
那么,该如何做呢?github.com/gorilla/mux 中有一个子路由(SubRouter)的方法,刚好可以解决这类问题
kit的日志功能,新建类型logMiddlewareServer,该类型中嵌入了Service,还包含一个logger属性
ServerOption:为服务器设置可选参数。
ServerErrorHandler用于处理非终端错误。默认情况下,非终端错误被忽略。这是一种诊断措施。细粒度控制错误处理,包括更详细的日志记录,应在自定义ServerErrorEncoder或ServerFinalizer,两者都可以访问上下文。
Method向HTTP方法的匹配器注册新路由。
Handler设置路由的处理程序。
Newserver构造一个实现http的新服务器。处理程序和包装,提供的终结点。
FormValue返回查询的命名组件的第一个值。POST和PUT正文参数优先于URL查询字符串值。FormValue在必要时调用ParseMultipartForm和ParseForm并忽略,这些函数返回的任何错误。如果键不存在,FormValue将返回空字符串。要访问同一个键的多个值,请调用ParseForm,然后检查请求。直接形成。
Set将与键关联的标题项设置为单元素值。它将替换任何现有值与键关联。密钥不区分大小写;它是由textproto规范化。CanonicalTimeHeaderKey。要使用非规范键,请直接指定给映射。