用 golang 编写一个短链接服务 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
ysz1121
V2EX    Go 编程语言

用 golang 编写一个短链接服务

  •  2
     
  •   ysz1121 2019-11-26 10:41:55 +08:00 7430 次点击
    这是一个创建于 2194 天前的主题,其中的信息可能已经有所发展或是发生改变。

    用 golang 编写一个短链接服务

    访问原文

    我们平时工作、生活中总会有各种各样的域名链接需要分享给同事或朋友或家人。但常常有域名的长度过长会有各种限制,或无法复制全而产生一些问题了,为了解决这个问题我们需要一个短链接生成器。

    基于上面的想法我写了一个短链接的生成器:

    https://github.com/icowan/shorter

    项目简介

    该服务基于 go-kit 组件进行开发,数据库基于 Redis 或 Mongo 进行存储。

    可以通过容器进行部署,也可以在 kubernetes 中进行部署。

    目录结构

    • cmd: 应用启动入口
    • dist: 前端静态文件目录
    • install: 安装目录
    • pkg
      • endpoint: 端点
      • http: 传输处理
      • logging: 日志中间件
      • repository: 仓库存储逻辑实现
      • service: 逻辑实现
    ├── Dockerfile ├── Makefile ├── README.md ├── cmd │ ├── main.go │ └── service ├── dist ├── go.mod ├── go.sum ├── install └── pkg ├── endpoint ├── http ├── logging ├── repository └── service 

    API

    该服务一共有两个接口,一个是生成短地址,另一个是短地址进行跳转。

    可以通过 Redis 进行数据存储也能通过 MongoDB 作为存储介质。

    Repository

    model

    存储的数据结构,主要三个字段

    • code: 生成的唯一码
    • url: 源地址
    • created_at: 创建时间
    // pkg/service/model.go type Redirect struct { Code string `json:"code"` URL string `json:"url"` CreatedAt time.Time `json:"created_at"` } 

    repository

    repository 提供了两个方法,一个是 Find 和 Store.

    • Find 根据 code 查询 url 信息
    • Store 存储 url 信息
    // pkg/service/repository.go type Repository interface { Find(code string) (redirect *Redirect, err error) Store(redirect *Redirect) error } 

    Repository 是一个 Interface 类型的结构体,没有具体实现。这里根据使用的存储数据库的不同需要实现不同的存储方式。

    • mongodb: pkg/repository/mongo/repository.go
    • redis: pkg/repository/redis/repository.go

    在启动入口 cmd/service/service.go 文件里可以看到启动是如何选择的:

    // cmd/service/service.go var repo service.Repository switch *dbDrive { case "mongo": repo, err = mongodb.NewMongoRepository(*mongoAddr, "redirect", 60) if err != nil { _ = level.Error(logger).Log("connect", "db", "err", err.Error()) return } case "redis": db, _ := strconv.Atoi(*redisDB) repo, err = redis.NewRedisRepository(redis.RedisDrive(*redisDrive), *redisHosts, *redisPassword, "shorter", db) if err != nil { _ = level.Error(logger).Log("connect", "db", "err", err.Error()) return } } 

    Service

    service 提供了两个方法 Get 和 Post。

    // pkg/service/service.go type Service interface { Get(ctx context.Context, code string) (redirect *Redirect, err error) Post(ctx context.Context, domain string) (redirect *Redirect, err error) } 
    • Get: 传入 code 码,根据 code 码去数据查找存储的地址信息
    • Post: 传入原 url 地址,生成 code 码并存入数据库,返回结构体

    Transport Post

    生成地址需要通过 POST 的方式传入 JSON 结构, 接收参考文件:

    // pkg/endpoint/endpoint.go type PostRequest struct { URL string `json:"url" validate:"required,url,lt=255"` } type dataResponse struct { Url string `json:"url"` Code string `json:"code"` CreatedAt time.Time `json:"created_at"` ShortUri string `json:"short_uri"` } type PostResponse struct { Err error `json:"err"` Data dataResponse `json:"data"` } 

    该接口只接收一个参数 "url",返回四个参数。

    • url: 原地址
    • short_uri: 跳转的短地址
    • code: 跳转短地址的 code
    • created_at: 生成时间

    Transport Get

    通过 uri 的 code 进行查询例如:

    r.Handle("/{code}", kithttp.NewServer( endpoints.GetEndpoint, decodeGetRequest, encodeGetResponse, options["Get"]...)).Methods( http.MethodGet) 

    解析 Request:

    func decodeGetRequest(_ context.Context, r *http.Request) (interface{}, error) { vars := mux.Vars(r) code, ok := vars["code"] if !ok { return nil, ErrCodeNotFound } req := endpoint.GetRequest{ Code: code, } return req, nil } 

    跳转:

    func encodeGetResponse(ctx context.Context, w http.ResponseWriter, response interface{}) (err error) { if f, ok := response.(endpoint.Failure); ok && f.Failed() != nil { ErrorRedirect(ctx, f.Failed(), w) return nil } resp := response.(endpoint.GetResponse) redirect := resp.Data.(*service.Redirect) http.Redirect(w, &http.Request{}, redirect.URL, http.StatusFound) return } // 错误跳回首页 func ErrorRedirect(_ context.Context, err error, w http.ResponseWriter) { http.Redirect(w, &http.Request{}, os.Getenv("SHORT_URI"), http.StatusFound) } 

    docker-compose 部署

    docker-compose 启动比较简单,直接进入目录install/docker-compose/

    然后执行:

    $ docker-compose up 

    在开普勒云平台进行部署

    开普勒平台演示地址: https://kplcloud.nsini.com/about.html 开普勒平台后端代码: https://github.com/kplcloud/kplcloud 开普勒平台安装教程

    由于此项目依赖数据库: Redis、MongoDB,所以在创建应用之前我们得先部署 Redis 或 MongoDB 的持久化应用。在项目里我给出了两数据库部署的 Demo,大家可以尝试在自己的环境中启动。

    开普勒云平台倾向于部署无状态的应用也就是 Deployment 类型,像这种需要持久化的应用最好是部署成有状态的应用如: StatefulSet 类型,相对来说比较好组成分布式集群或主从节构。

    单点 Redis 服务: install/kubernetes/redis/ 单点 MongoDB 服务: install/kubernetes/mongo/

    创建一个应用

    我们创建一个名叫shorter的应用:

    1. 输入 github 的地址: icowan/shorter
    2. 选择版本: v0.1.8
    3. 选择启动的容器数量: 2
    4. 最大64Mi内存,应用比较简单不需要太大的使用内存
    5. 启动的端口: 8080
    6. 提交管理员审核

    管理员审核、初始化发布应用

    管理员收到通知后进入基础详情页进行审核:

    主要查看提交的基础信息是否正确,自动生成的 YAML 文件是否正确,自动生成的 Jenkins 模版是否正确及用户项目里的 Dockerfile 文件是否有误,若没有问题点击**“开始部署”**按钮直接进行应用的构建及发布。

    应用部署成功后,系统会向像的邮件、微信发送通知,告知应用发布的情况。(微信通知需要您在“个人设置”->“账号绑定”->“绑定(关注微信公众号即可自动绑定)”->“消息订阅设置” 在消息订阅里勾选需要通知的类型及方式)详情请看文档:

    若收到成功发布的信息,那应用就算是启动成功了。

    后续若要升级应用,应用创建者或组成员可以直接在应用详情页选择**“Build”**按钮并且选择相应用版本就好了。

    回滚应用也同样方便:

    只需要点击**“回滚”**按钮,在弹出的窗口选择所需要回滚的版本,点击 [“回滚”] 并确认,平台会将该版本的 Docker Image 进行启动。

    生成外问地址

    完成之后,为了让外部可以访问到该代理,需要生成一个对外可访问的地址。

    在应用详情的最下方有一个“外部地址”的卡片,若是第一次创建应用,在卡片的 header 的右边有一个有**“添加”**按钮点击它,并确认就可以生成一个外部地址了。

    上面就是生成的地址,我们可以通过这个地址访问到 shorter 应用。

    测试

    我这部署了一个生通过短域名解析到该应用上的例子,点击下面地址进行短链生成页面。

    把需要生成短链接的地址贴到输入框,并点击**“生成短链”**按钮即可生成。

    点击**“复制”** 按钮即可将短地址复制并使用。

    尾巴

    golang 语言是一个非常高效且简单易学的编程语言,基于 golang 语言的特性,我们可以写出非常多有意思的工具或平台。

    你的打赏就是我更新的动力

    36 条回复    2020-08-06 14:39:11 +08:00
    Vegetable
        1
    Vegetable  
       2019-11-26 10:44:05 +08:00
    不错哦,不过你这个 nsini 的域名,我总会联想到脏话...
    ysz1121
        2
    ysz1121  
    OP
       2019-11-26 10:45:22 +08:00
    @Vegetable 哈哈哈 不用在意这些细节
    Ritter
        3
    Ritter  
       2019-11-26 11:28:09 +08:00
    star 了 学习学习
    SharkIng
        4
    SharkIng  
       2019-11-26 11:34:27 +08:00
    想找一个 Go 的短网址终于有了
    keepeye
        5
    keepeye  
       2019-11-26 11:45:29 +08:00
    自己的域名在微信里面传播分分钟被封杀
    建议提供 jsonp 接口,用于在第三方网页中通过 id 获取跳转目标,第三方比如 kuaizhan
    opengps
        6
    opengps  
       2019-11-26 11:55:06 +08:00
    短网址是个非常容易滥用的东西,所以我也最终选择了自建
    ysz1121
        7
    ysz1121  
    OP
       2019-11-26 11:56:33 +08:00
    @keepeye 好使吗?
    ysz1121
        8
    ysz1121  
    OP
       2019-11-26 11:56:53 +08:00
    @opengps 优秀
    Leigg
        9
    Leigg  
       2019-11-26 12:00:51 +08:00 via Android
    核心技术是什么?
    CEBBCAT
        10
    CEBBCAT  
       2019-11-26 12:47:05 +08:00
    good job,另外这算不上算给 CI 打广告呀?哈哈
    lhx2008
        11
    lhx2008  
       2019-11-26 12:52:00 +08:00 via Android
    这个 uri 有点长。。
    ysz1121
        12
    ysz1121  
    OP
       2019-11-26 13:05:23 +08:00
    @lhx2008 再注册一个短的还得备案太麻烦了
    catror
        13
    catror  
       2019-11-26 13:08:54 +08:00 via Android
    我也用 Go 写了一个,公司产品在用
    lhx2008
        14
    lhx2008  
       2019-11-26 13:09:44 +08:00 via Android
    @ysz1121 不是域名,是斜杠后面的
    ylsc633
        15
    ylsc633  
       2019-11-26 13:25:17 +08:00
    只有我注意到了这个吗?

    开普勒云平台 https://github.com/kplcloud/kplcloud
    ysz1121
        16
    ysz1121  
    OP
       2019-11-26 13:26:06 +08:00
    @ylsc633 优秀
    wzw
        17
    wzw  
       2019-11-26 13:41:06 +08:00 via iPhone
    能不能自定义
    heiheidewo
        18
    heiheidewo  
       2019-11-26 13:55:28 +08:00
    短链接的核心问题是怎么防止被微信和 QQ 封禁,这种怎么解决呢?
    ysz1121
        19
    ysz1121  
    OP
       2019-11-26 14:41:59 +08:00
    @wzw 自己定义什么?域名吗?设置 环境变量就好了
    ysz1121
        20
    ysz1121  
    OP
       2019-11-26 14:43:08 +08:00
    @heiheidewo 我试了是好使的
    heiheidewo
        21
    heiheidewo  
       2019-11-26 14:45:02 +08:00
    @ysz1121 那是因为刚使用没几个人用来推广,等其他人来用你的短链接服务的时候就知道了
    iamfirst
        22
    iamfirst  
       2019-11-26 14:54:31 +08:00
    先 star 为敬
    wzw
        23
    wzw  
       2019-11-26 15:24:40 +08:00
    @ysz1121 #19 自定义 KYqwkExZR 这部分


    https://r.nsini.com/KYqwkExZR
    ClarkAbe
        24
    ClarkAbe  
       2019-11-26 16:05:28 +08:00 via Android
    嵌入式 kv+一个静态 page,一个生成 api,读取非空的“/”get 请求就能搞定的事情为什么要搞这么麻烦
    realpg
        25
    realpg  
    PRO
       2019-11-26 16:20:07 +08:00   1
    一个感觉就是真特么长。

    域名和文章都是
    ysz1121
        26
    ysz1121  
    OP
       2019-11-26 17:43:36 +08:00
    @wzw 嗯.... 这是个好功能,可以考虑支持一下
    ysz1121
        27
    ysz1121  
    OP
       2019-11-26 17:54:15 +08:00
    @realpg 哈哈哈 哈哈 这不为了水文单嘛,直接看 README 会更简单一些
    ysz1121
        28
    ysz1121  
    OP
       2019-11-26 17:56:34 +08:00
    @ClarkAbe 复杂吗?就两 API 很简单啦, 这么写是为了方便部署 并且本就是微服务,若需要另入到微服务网中去也比较简单
    f1ren2es
        29
    f1ren2es  
       2019-11-26 23:19:05 +08:00
    @Leigg url 做摘要提取,存储映射,google 一下一把方案
    f1ren2es
        30
    f1ren2es  
       2019-11-26 23:19:40 +08:00   1
    实际上用文件存储会更轻量: https://github.com/etcd-io/bbolt
    ggicci
        31
    ggicci  
       2019-11-27 02:54:57 +08:00
    我想跟楼主做盆友
    ysz1121
        32
    ysz1121  
    OP
       2019-11-27 09:52:56 +08:00
    @f1ren2es 学习了
    fiypig
        33
    fiypig  
       2019-11-27 14:28:55 +08:00
    马克
    wzw
        34
    wzw  
       2020-05-11 10:28:41 +08:00
    @f1ren2es #30 为啥没选 badger
    MartinMusic
        35
    MartinMusic  
       2020-06-08 16:35:51 +08:00
    我知道国内还有个挺不错的短网址服务平台,可以接入自己的域名,也可以研究一下 ,叫米发
    nanhuai
        36
    nanhuai  
       2020-08-06 14:39:11 +08:00
    写的不错
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     4598 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 52ms UTC 09:48 PVG 17:48 LAX 01:48 JFK 04:48
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86