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 · 6817 次点击
    这是一个创建于 1603 天前的主题,其中的信息可能已经有所发展或是发生改变。

    用 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  
       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   ·   我们的愿景   ·   实用小工具   ·   4077 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 10:20 · PVG 18:20 · LAX 03:20 · JFK 06:20
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.