go微服务系列(二):go-kit开发web应用


通过开发一个 User Web 应用来学习如何进行 Go Web 项目开发

使用 Go Modules 管理项目依赖

在前面的课时中,我们演示的 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 应用

接下来我们就基于 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 应用的项目结构如下图所示:
(user目录下)
在这里插入图片描述

由图我们可以看到 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/kit@v0.10.0 // Go -k it 框架 
go get github.com/go-redsync/redsync@v1.4.2 // Redis 分布式锁 
go get github.com/go-sql-driver/mysql@v1.5.0 // mysql 驱动 
go get github.com/gomodule/redigo@v2.0.0+incompatible // redis 客户端 
go get github.com/gorilla/mux@v1.7.4 // mux 路由 
go get github.com/jinzhu/gorm@v1.9.14 // gorm mysql orm 框架

你也可以直接修改go.mod文件,添加如下内容,然后go mod init:

github.com/go-kit/kit@v0.10.0 // Go -k it 框架 
github.com/go-redsync/redsync@v1.4.2 // Redis 分布式锁 
github.com/go-sql-driver/mysql@v1.5.0 // mysql 驱动 
github.com/gomodule/redigo@v2.0.0+incompatible // redis 客户端 
github.com/gorilla/mux@v1.7.4 // mux 路由 
github.com/jinzhu/gorm@v1.9.14 // gorm mysql orm 框架

如果出现这些包在go.mod中报错,红色显示,报错unresolve dependency:
在这里插入图片描述
一般来说版本问题不大,出问题你可以跟句你的go版本来查看官网,对应包需要的版本

接下来我们就按照 service、endpoint、transport 和 main 的顺序构建整个项目。

service包

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包

在 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包

在 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函数

最后是在 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);

使用 gorm 连接 My SQL 数据库

在日常的业务开发中,使用数据库对业务数据进行持久化操作是必不可少的。在前面的 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 
}

源码

main.go

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包停止向通道发送信号,通道不会在接受到任何信号。
信号类别:
在这里插入图片描述

mysql.go

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将占位符传入的变量返回为字符串,不显示在终端。

user_dao.go

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

user_endpoint.go

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是一个函数的别名,返回值有两个

redis.go

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:互斥,分布式锁

user_service.go

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:返回“记录未找到错误”。仅在尝试使用结构查询数据库时发生;使用切片进行查询不会返回此错误

user_service_test.go

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(), "aoho@mail.com", "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:"aoho@mail.com",
		})

	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: 测试时并行执行.

http.go

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。要使用非规范键,请直接指定给映射。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿白,

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值