这一节我们重现一下go-zero仓库中的短链接服务案例:短链接服务
短链接服务的流程一般是这样的
这里为了简单点我们就模拟一个生成短链接key的接口以及根据key去换取原url地址的接口
准备工作
这个案例我们要用到mysql、redis、以及etcd,这里为了简单我就用docker安装一下:
#安装etcd服务端 | |
docker pull bitnami/etcd:3.5 | |
docker run -d --name Etcd-server | |
--publish 2379:2379 | |
--publish 2380:2380 | |
--env ALLOW_NONE_AUTHENTICATION=yes | |
--env ETCD_ADVERTISE_CLIENT_URLS=http://etcd-server:2379 | |
bitnami/etcd:3.5 | |
#安装mysql | |
docker run -p 3310:3306 --name mysql5.7 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7 | |
#安装redis | |
docker run -di --name=redis -p 6379:6379 redis |
docker ps看到我们这几个容器启动起来了就行
goland创建有一个新项目shorturl-service
编写api代码
在根目录下新建api文件夹并在api文件下执行
命令最好都自己敲一次不要复制粘贴,能增加一点熟练度
goctl api -o shorturl.api
基于这个模板我们写自己想要的内容就好了
修改后的api文件内容如下:
syntax = "v1" | |
//获取短链接的request跟response结构 | |
type getShortUrlRequest { | |
Url string `json:"url"` | |
} | |
type getShortUrlResponse { | |
ShortUrl string `json:"short_url"` | |
} | |
//获取长链接的request跟response结构 | |
type getLongUrlRequest { | |
ShortUrl string `json:"short_url"` | |
} | |
type getLongUrlResponse { | |
Url string `json:"url"` | |
} | |
service shorturl-api { | |
GetShortUrl | |
post /short(getShortUrlRequest) returns (getShortUrlResponse) | |
GetLongUrl | |
post /long(getLongUrlRequest) returns (getLongUrlResponse) | |
} |
然后我们通过goctl来生成api的代码,在api文件夹下执行下面的命令
goctl api go -api shorturl.api -dir .
然后会看到这样的目录结构
到了这里可能有一些文件会飘红
我们在命令行下执行go mod tidy
把依赖都引入一下就好了
我们打开logic目下的两个文件
可以看到这里有提示 我们只需要在这里补充我们的逻辑代码就好了。
我们先来简单改写一下:
func (l *GetShortUrlLogic) GetShortUrl(req *types.GetShortUrlRequest) (resp *types.GetShortUrlResponse, err error) { | |
return &types.GetShortUrlResponse{ | |
ShortUrl: "return short_url test", | |
}, nil | |
} | |
func (l *GetLongUrlLogic) GetLongUrl(req *types.GetLongUrlRequest) (resp *types.GetLongUrlResponse, err error) { | |
return &types.GetLongUrlResponse{ | |
Url: "return long_url test", | |
}, nil | |
} |
ok我们现在就来启动这个api服务
在api文件夹下执行
#以指定配置文件的方式启动 | |
go run shorturl.go -f etc/shorturl-api.yaml |
这样我们的服务就启动起来了,可以看到正在监听着8888端口
我们可以看下shorturl.go
的代码
如果你不通过-f
指定后面的配置文件的话 将会使用etc/shorturl-api.yaml作为配置文件。
下面我们通过postman请求一下写好的两个接口:
OK已经正常返回我们设定的值了,下面我们将数据库的操作加入进来
我们在数据库中新建一个shorturl-service的数据库,并创建一个shorturl的表
CREATE TABLE `shorturl` ( | |
`id` int(11) NOT NULL AUTO_INCREMENT, | |
`short_url` varchar(255) NOT NULL COMMENT '生成的key', | |
`url` varchar(255) NOT NULL COMMENT '原始的url', | |
PRIMARY KEY (`id`), | |
KEY `short_url` (`short_url`) USING BTREE | |
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
在api文件夹下新建model文件夹并新建shorturl.sql文件,文件内容就是上面的sql
然后我们在model文件夹下执行下面的命令用来生成 CRUD+cache 代码,-c 表示使用 redis cache
goctl model mysql ddl -c -src shorturl.sql -dir . | |
api/model | |
├── shorturl.sql | |
├── shorturlmodel.go // 扩展代码 | |
├── shorturlmodel_gen.go // CRUD+cache 代码 | |
└── vars.go // 定义常量和变量 |
因为我们后面要根据short_url去查询记录,所以我们在shorturlmodel_gen.go文件下增加FindOneByShortUrl
方法
修改api/model/shorturlmodel_gen.go文件
shorturlModel interface { | |
Insert(ctx context.Context, data *Shorturl) (sql.Result, error) | |
FindOne(ctx context.Context, id int64) (*Shorturl, error) | |
Update(ctx context.Context, data *Shorturl) error | |
Delete(ctx context.Context, id int64) error | |
FindOneByShortUrl(ctx context.Context, shortUrl string) (*Shorturl, error) //新增方法 | |
} | |
//新增方法 | |
func (m *defaultShorturlModel) FindOneByShortUrl(ctx context.Context, shortUrl string) (*Shorturl, error) { | |
shorturlIdKey := fmt.Sprintf("%s%v", cacheShorturlIdPrefix, shortUrl) | |
var resp Shorturl | |
err := m.QueryRowCtx(ctx, &resp, shorturlIdKey, func(ctx context.Context, conn sqlx.SqlConn, v interface{}) error { | |
query := fmt.Sprintf("select %s from %s where `short_url` = ? limit 1", shorturlRows, m.table) | |
return conn.QueryRowCtx(ctx, v, query, shortUrl) | |
}) | |
switch err { | |
case nil: | |
return &resp, nil | |
case sqlc.ErrNotFound: | |
return nil, ErrNotFound | |
default: | |
return nil, err | |
} | |
} |
在etc/shorturl-api.yaml
文件增加以下配置
DataSource: root:123456@tcp(localhost:3310)/shorturl-service | |
Table: shorturl | |
Cache: | |
- Host: localhost:6379 |
修改 api/internal/config/config.go,如下:
type Config struct { | |
rest.RestConf | |
DataSource string //增加代码 | |
Table string //增加代码 | |
Cache cache.CacheConf //增加代码 | |
} |
修改 api/internal/svc/servicecontext.go,如下:
type ServiceContext struct { | |
Config config.Config | |
Model model.ShorturlModel //增加代码 | |
} | |
func NewServiceContext(c config.Config) *ServiceContext { | |
return &ServiceContext{ | |
Config: c, | |
Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache),//增加代码 | |
} | |
} |
重写api/internal/logic/getshorturllogic.go的GetShortUrl方法
func (l *GetShortUrlLogic) GetShortUrl(req *types.GetShortUrlRequest) (resp *types.GetShortUrlResponse, err error) { | |
key := hash.Md5Hex([]byte(req.Url))[:6] | |
_, err = l.svcCtx.Model.Insert(l.ctx, &model.Shorturl{ | |
ShortUrl: key, | |
Url: req.Url, | |
}) | |
if err != nil { | |
return nil, err | |
} | |
return &types.GetShortUrlResponse{ | |
ShortUrl: key, | |
}, nil | |
} |
重写api/internal/logic/getlongurllogic.go的GetLongUrl方法
func (l *GetLongUrlLogic) GetLongUrl(req *types.GetLongUrlRequest) (resp *types.GetLongUrlResponse, err error) { | |
result, err := l.svcCtx.Model.FindOneByShortUrl(l.ctx, req.ShortUrl) | |
if err != nil { | |
return nil, err | |
} | |
return &types.GetLongUrlResponse{ | |
Url: result.Url, | |
}, nil | |
} |
重新启动服务
go run shorturl.go
再次请求接口
到这里我们的功能就已经实现了,在一个项目初期一般也都是只提供api接口就ok了,当项目变大后,我们可以将其扩展为微服务,下面我们改写一下,就不在logic直接调用model去操作数据库了,改为调用rpc服务去实现功能。
在项目根目录下新建rpc文件夹,在rpc文件夹下新建transform文件夹,在transform文件夹下执行下面的命令生成proto文件
goctl rpc template -o transform.proto
生成的proto模板文件是这样的
syntax = "proto3"; | |
package transform; | |
option go_package="./transform"; | |
message Request { | |
string ping = 1; | |
} | |
message Response { | |
string pong = 1; | |
} | |
service Transform { | |
rpc Ping(Request) returns(Response); | |
} |
我们跟上面的api文件一样根据自己的需求改写一下:
syntax = "proto3"; | |
package transform; | |
option go_package="./transform"; | |
message GetShortUrlRequest { | |
string url = 1; | |
} | |
message GetShortUrlResponse { | |
string short_url = 1; | |
} | |
message GetLongUrlRequest { | |
string short_url = 1; | |
} | |
message GetLongUrlResponse { | |
string url = 1; | |
} | |
service Transform { | |
rpc GetShortUrl(GetShortUrlRequest) returns(GetShortUrlResponse); | |
rpc GetLongUrl(GetLongUrlRequest) returns(GetLongUrlResponse); | |
} |
然后用goctl生成rpc的代码,在transform目录下执行
goctl rpc protoc transform.proto --go_out=. --go-grpc_out=. --zrpc_out=.
文件结构如下:
rpc/transform | |
├── etc | |
│ └── transform.yaml // 配置文件 | |
├── internal | |
│ ├── config | |
│ │ └── config.go // 配置定义 | |
│ ├── logic | |
│ │ ├── getlongurllogic.go // longurl 业务逻辑在这里实现 | |
│ │ └── getshorturllogic.go // shorturl 业务逻辑在这里实现 | |
│ ├── server | |
│ │ └── transformerserver.go // 调用入口, 不需要修改 | |
│ └── svc | |
│ └── servicecontext.go // 定义 ServiceContext,传递依赖 | |
├── transform | |
│ ├── transform.pb.go | |
│ └── transform_grpc.pb.go | |
├── transform.go // rpc 服务 main 函数 | |
├── transform.proto | |
└── transformclient | |
└── transform.go // 提供了外部调用方法,无需修改 |
我们将上面api文件夹下面的model文件夹剪切到rpc文件夹下,然后把对应的配置也加上
transform.yaml加
DataSource: root:123456@tcp(localhost:3310)/shorturl-service | |
Table: shorturl | |
Cache: | |
- Host: localhost:6379 |
transform/internal/config/config.go修改
type Config struct { | |
zrpc.RpcServerConf | |
DataSource string // 手动代码 | |
Table string // 手动代码 | |
Cache cache.CacheConf // 手动代码 | |
} |
transform/internal/svc/servicecontext.go修改
type ServiceContext struct { | |
Config config.Config | |
Model model.ShorturlModel //增加代码 | |
} | |
func NewServiceContext(c config.Config) *ServiceContext { | |
return &ServiceContext{ | |
Config: c, | |
Model: model.NewShorturlModel(sqlx.NewMysql(c.DataSource), c.Cache), //增加代码 | |
} | |
} |
然后把我们的logic文件补充一下:
getlongurllogic的GetLongUrl改为:
func (l *GetLongUrlLogic) GetLongUrl(in *transform.GetLongUrlRequest) (*transform.GetLongUrlResponse, error) { | |
result, err := l.svcCtx.Model.FindOneByShortUrl(l.ctx, in.ShortUrl) | |
if err != nil { | |
return nil, err | |
} | |
return &transform.GetLongUrlResponse{ | |
Url: result.Url, | |
}, nil | |
} |
getshorturllogic的GetShortUrl改为:
func (l *GetShortUrlLogic) GetShortUrl(in *transform.GetShortUrlRequest) (*transform.GetShortUrlResponse, error) { | |
key := hash.Md5Hex([]byte(in.Url))[:6] | |
_, err := l.svcCtx.Model.Insert(l.ctx, &model.Shorturl{ | |
ShortUrl: key, | |
Url: in.Url, | |
}) | |
if err != nil { | |
return nil, err | |
} | |
return &transform.GetShortUrlResponse{ | |
ShortUrl: key, | |
}, nil | |
} |
ok rpc这部分就改完了,我们去修改一下api的部分,将原本调用model实现的逻辑改为调用rpc服务去实现
修改api/etc/shorturl-api.yaml文件
//新增 | |
//通过 etcd 自动去发现可用的 transform 服务 | |
Transform: | |
Etcd: Hosts: - localhost:2379 | |
Key: transform.rpc |
修改api/internal/config/config.go文件
type Config struct { | |
rest.RestConf | |
Transform zrpc.RpcClientConf //新增代码 | |
} |
修改api/internal/svc/servicecontext.go文件
type ServiceContext struct { | |
Config config.Config | |
Transformer transformclient.Transform //新增代码 | |
} | |
func NewServiceContext(c config.Config) *ServiceContext { | |
return &ServiceContext{ | |
Config: c, | |
Transformer: transformclient.NewTransform(zrpc.MustNewClient(c.Transform)), //新增代码 | |
} | |
} |
修改我们的logic文件,改为调用rpc服务
getlongurllogic的GetLongUrl改为:
func (l *GetLongUrlLogic) GetLongUrl(req *types.GetLongUrlRequest) (resp *types.GetLongUrlResponse, err error) { | |
ret, err := l.svcCtx.Transformer.GetLongUrl(l.ctx, &transform.GetLongUrlRequest{ | |
ShortUrl: req.ShortUrl, | |
}) | |
if err != nil { | |
return nil, err | |
} | |
return &types.GetLongUrlResponse{ | |
Url: ret.Url, | |
}, nil | |
} |
getshorturllogic的GetShortUrl改为:
func (l *GetShortUrlLogic) GetShortUrl(req *types.GetShortUrlRequest) (resp *types.GetShortUrlResponse, err error) { | |
ret, err := l.svcCtx.Transformer.GetShortUrl(l.ctx, &transform.GetShortUrlRequest{ | |
Url: req.Url, | |
}) | |
if err != nil { | |
return nil, err | |
} | |
return &types.GetShortUrlResponse{ | |
ShortUrl: ret.ShortUrl, | |
}, nil | |
} |
ok 我们重启api服务跟rpc服务
#在api文件夹下执行 | |
go run shorturl.go | |
#在rpc/transform文件夹下执行 | |
go run transform.go |
然后我们再调一下接口:
可以看到我们的功能实现逻辑已经改在rpc服务这边去实现了