使用Go语言开发一个短链接服务:三、项目目录结构设计
章节
Gitee https://gitee.com/alxps/short_link
Github https://github.com/1911860538/short_link
上一篇我们讨论了项目基本架构以及组件选择。这篇我们讲一下项目目录结构。
目录结构
目录结构大致如下:
.
├── app
│ ├── component # 组件
│ │ ├── cache.go # 缓存interface
│ │ ├── cache_redis.go # 缓存redis实现
│ │ ├── database.go # 数据库interface
│ │ ├── database_mongodb.go # 数据库mongodb实现
│ │ └── lifespan.go # 组件生命周期管理
│ └── server # Web业务主要逻辑
│ ├── handler # handler层
│ │ ├── add_link.go # 添加短链接handler
│ │ ├── get_link.go # 获取链接详情handler
│ │ ├── jwt.go # handler处理认证jwt公共逻辑
│ │ └── redirect.go # 短链接跳转handler
│ ├── middleware # 中间件
│ │ └── auth.go # 认证中间件
│ ├── router.go # url与handler路由
│ └── service # handler对于的业务逻辑实现
│ ├── add_link.go
│ ├── get_link.go
│ └── redirect.go
├── cmd
│ └── server
│ └── main.go # http server启动文件
├── config # 配置文件
│ ├── config_dev.json
│ ├── config.go
│ ├── config_prod.json
│ └── init.go
├── Dockerfile
├── docs
│ └── api_doc.json # api接口文档
├── go.mod
├── go.sum
├── LICENSE
├── Makefile
├── README.md
├── tests
│ └── service # app/server/service对应的单元测试
│ ├── add_link_test.go
│ ├── get_link_test.go
│ ├── mock_cache.go # app/component/cache interface单元测试mock实现
│ ├── mock_database.go # app/component/database interface单元测试mock实现
│ ├── mock_lifespan.go # app/component/lifespan interface单元测试mock实现
│ └── redirect_test.go
具体代码点击这里。
上面代码目录中主要讲一下app内几个目录作用。
1、app/component为项目依赖的组件,组件方法interface以及实现。
2、app/server/handler为接口层,负责request参数处理,调用service逻辑并处理response数据
3、app/server/service,业务逻辑实现
app/component这里定义组件相关interface,主要为了方便单元测试。不由想起鲁迅的话:“Don't design with interfaces, discover them.”。比如我们添加短链接信息add_link,依赖数据库。
app/server/service/add_link.go
package service
import (
"context"
"net/http"
"github.com/1911860538/short_link/app/component"
)
type AddLinkSvc struct {
Database component.DatabaseItf
}
type AddLinkParams struct {
UserId string
LongUrl string
Deadline time.Time
}
type AddLinkRes struct {
StatusCode int
Msg string
Code string
}
// ...省略代码
func (s *AddLinkSvc) Do(ctx context.Context, params AddLinkParams) (AddLinkRes, error) {
// ...省略代码
return AddLinkRes{
StatusCode: http.StatusCreated,
Code: code,
}, nil
}
app/component/database.go
package component
import (
"context"
"log"
"github.com/1911860538/short_link/config"
)
type DatabaseItf interface {
Lifespan
Create(ctx context.Context, link *Link) (id string, codeExisted bool, err error)
Get(ctx context.Context, params map[string]any) (*Link, error)
}
var Database DatabaseItf
func init() {
switch databaseType := config.Conf.Server.DbType; databaseType {
case "mongodb":
Database = DefaultMongoDB
default:
log.Fatalf("不支持的数据库组件:%s\n", databaseType)
}
}
AddLinkSvc无需依赖特定数据库,只需实现了对应interface。在实际handler逻辑中我们使用实现了interface的mongodb
app/server/handler/add_link.go
package handler
import (
"log/slog"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/1911860538/short_link/app/component"
"github.com/1911860538/short_link/app/server/service"
)
var addLinkSvc = service.AddLinkSvc{
Database: component.Database,
}
// ...省略代码
func AddLinkHandler(c *gin.Context) {
// ...省略代码
res, err := addLinkSvc.Do(c.Request.Context(), addLinkParams)
// ...省略代码
}
而对AddLinkSvc单元测试时,使用实现了interface的mock Database。不由想起刚入门go时,函数逻辑有数据库相关操作,要对这个函数写单元测试时的心情:
停杯投箸不能食,拔剑四顾心茫然。
欲渡黄河冰塞川,将登太行雪满山。
实现API接口
之前文章介绍过短链接的基本原理( 使用Go语言开发一个短链接服务:一、基本原理 ),这里简单回顾一下。用户有一个长链接接Looooong,需要一个短的链接S映射并跳转到Looooong。因此需要实现下面3个接口
1、用户申请短链接,输入长链接,服务返回对应短链接,并保存两者映射关系;
2、用户获取自己申请的短链接详情;
3、跳转服务,用户申请获得的短链接经本服务调转到对应的长链接
app/server/router.go
package server
import (
"github.com/gin-gonic/gin"
"github.com/1911860538/short_link/app/server/handler"
"github.com/1911860538/short_link/app/server/middleware"
)
// Route 路由注册
func Route(e *gin.Engine) {
// 核心,跳转服务
e.GET("/:code", handler.RedirectHandler)
// 短链接管理
linKGroupV1 := e.Group("/api/v1/links")
linKGroupV1.Use(middleware.JwtMiddleware)
{
linKGroupV1.POST("", handler.AddLinkHandler)
linKGroupV1.GET("", handler.GetLinkHandler)
}
}
数据库设计
orm结构体
app/component/database_mongodb.go
// Link
/*
对于添加索引操作,官方go驱动不能在结构体tag赋值完成
需要在该collection创建了,并包含至少一个document,才能添加索引
code唯一索引: db.links.createIndex({"code": 1}, {"unique": true})
user_id普通索引: db.links.createIndex({"user_id": 1})
long_url普通索引: db.links.createIndex({"long_url": 1})
ttl_time ttl索引: db.links.createIndex({"ttl_time": 1}, {"expireAfterSeconds": 7200})
// ttl索引会增加数据库负载。如果不使用ttl索引,可以用定时脚本任务删除无用数据
*/
type Link struct {
Id string `bson:"_id,omitempty"`
UserId string `bson:"user_id"` // 用户id
Code string `bson:"code"` // 短链接code
Salt string `bson:"salt"` // 生成code算法可能需要的盐
LongUrl string `bson:"long_url"` // 跳转目标长链接
Deadline time.Time `bson:"deadline"` // 短链接有效期
TtlTime time.Time `bson:"ttl_time"` // 本条数据删除时间
CreatedAt time.Time `bson:"created_at"`
UpdatedAt time.Time `bson:"updated_at"`
}
对应mongodb,database为short_link,collection为links。
>> use short_link
switched to db short_link
>> db.links.find().sort({"created_at": -1}).limit(1)
{
_id: ObjectId('65f40d43b5f826547e721ef7'),
user_id: '1f70a466-1449-4676-b2d7-2037341c718e',
code: 'Y64CyP',
salt: '',
long_url: 'https://juejin.cn/post/7346009985770471451',
deadline: 2024-03-15T19:12:51.000Z,
ttl_time: 2024-04-14T19:12:51.000Z,
created_at: 2024-03-15T08:56:35.778Z,
updated_at: 2024-03-15T08:56:35.778Z
}
用户系统
本项目接口API中,添加链接和获取链接两个接口需要用户登录后才能操作,这两个接口使用了中间件验证请求头必须有认证JsonWebToken。
然而,项目并没有实现用户登录注册等逻辑。原因有三
1、项目主要用来展示短链接的实现,登录注册非主要目的
2、实际项目中,用户服务通常是独立于其它服务的基础服务,而用到用户信息的服务通常只实现认证逻辑
3、我不想写
认证jwt解析使用了,github.com/golang-jwt/jwt/v5
总结
好了,上面大致介绍了项目主要目录以及各目录职责。下一篇将阐述生成短链接code的算法。