如何在Golang中实现URL短链接服务

来源:Nodejs社区作者:辉辉头衔:草根站长
导读:本期聚焦于辉辉创作的《如何在Golang中实现URL短链接服务》,敬请观看详情。URL短链接服务是日常开发中常见的功能需求,能够将长网址转换为简短易记的短链接,方便传播和统计。很多开发者想要了解如何使用Golang快速实现这类服务。本文将从核心原理出发,讲解短链生成逻辑、存储方案选择、HTTP接口设计等关键环节,结合具体代码示例演示完整实现过程,同时介绍缓存优化、冲突处理等实用技巧,帮助开发者掌握Golang下URL短链接服务的开发方法,满足实际业务场景的使用需求。

URL短链接服务的核心目标是把一个较长的网址转换成较短的访问入口,并在用户访问短入口时准确还原到原始地址。这类服务在活动推广、消息分享、短信通知和后台跳转等场景中非常常见。由于短链接本身承担入口职责,因此在实现时需要同时关注唯一性读取性能存储可靠性以及后续扩展能力。使用 Golang 实现具备天然优势,协程模型可以支撑较高并发,标准库和生态组件也能方便地构建 HTTP 服务。

短链接服务的工作机制与设计重点

短链接服务表面上只是把一个长网址变短,实际上包含两个关键流程。第一个流程是生成流程,服务接收客户端提交的长网址,为其分配一个全局唯一的短标识,再把短标识和原始网址的映射关系保存下来。第二个流程是跳转流程,服务从请求路径中取出短标识,查找对应的原始网址,并通过重定向把访问者引导到目标页面。生成流程强调唯一性和写入稳定,跳转流程强调查询速度和命中率。

在短标识生成方式上,常见思路包括哈希摘要和自增编号两类。哈希摘要可以直接根据长网址计算出一个固定长度的字符串,实现简单,但不同输入可能产生相同结果,因此必须处理冲突。自增编号配合 Base62 编码则更加稳定,编号由统一存储或缓存统一分配,天然不会重复。编号经过 Base62 转换后,可以得到由数字、小写字母和大写字母组成的短字符串,长度可控,也便于在浏览器地址中使用。

在存储设计上,短链接映射关系既需要持久化,也需要高速读取。MySQL 适合保存最终数据,能够提供事务能力和关系查询能力。Redis 适合承担缓存职责,可以把高频访问的短标识直接放在内存中。写入时先落库再写缓存,可以保证数据有持久化兜底。读取时先查 Redis,未命中再查 MySQL 并回填缓存,可以显著降低数据库压力。这种组合在当下是比较稳妥的短链接服务实现方式。

使用 Golang 实现短链接服务的完整思路

在技术选型上,可以使用 gin 提供 HTTP 路由,使用 gorm 操作 MySQL,使用 go-redis 访问 Redis。三者组合可以快速搭建结构清晰的服务。项目初始化阶段需要创建 Go 模块,并安装依赖包。执行以下命令可以初始化模块并拉取常用组件。

# 初始化项目并安装依赖
go mod init short_url
go get github.com/gin-gonic/gin
go get gorm.io/gorm
go get gorm.io/driver/mysql
go get github.com/go-redis/redis/v8

核心代码可以组织为一个完整的服务入口。服务启动时初始化 MySQL 与 Redis,注册生成接口和跳转接口。生成接口从表单中读取长网址,通过 Redis 的 INCR 命令获得自增编号,再调用 encodeBase62 将编号转换为短标识。随后,映射关系写入 MySQL,并同步写入 Redis。跳转接口优先从 Redis 读取原始网址,如果缓存不存在,再回到 MySQL 查询,并将查询结果重新写入缓存。

package main

import (
    "context"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/go-redis/redis/v8"
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var (
    db          *gorm.DB
    redisClient *redis.Client
    ctx         = context.Background()
)

const base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

// ShortURL 保存短标识与原始长网址的映射关系
type ShortURL struct {
    gorm.Model
    LongURL  string `gorm:"type:text;not null" json:"long_url"`
    ShortKey string `gorm:"type:varchar(20);uniqueIndex;not null" json:"short_key"`
}

// encodeBase62 将自增编号转换为 Base62 短字符串
func encodeBase62(num int64) string {
    if num == 0 {
        return "0"
    }
    result := ""
    for num > 0 {
        remainder := int(num % 62)
        result = string(base62Chars[remainder]) + result
        num = num / 62
    }
    return result
}

// initMySQL 初始化数据库连接并迁移表结构
func initMySQL() error {
    dsn := "root:password@tcp(127.0.0.1:3306)/short_url?charset=utf8mb4&parseTime=True&loc=Local"
    var err error
    db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        return err
    }
    return db.AutoMigrate(&ShortURL{})
}

// initRedis 初始化 Redis 连接
func initRedis() error {
    redisClient = redis.NewClient(&redis.Options{
        Addr:     "127.0.0.1:6379",
        Password: "",
        DB:       0,
    })
    return redisClient.Ping(ctx).Err()
}

// generateShortURL 接收长网址并生成短链接
func generateShortURL(c *gin.Context) {
    longURL := c.PostForm("long_url")
    if longURL == "" {
        c.JSON(http.StatusBadRequest, gin.H{"error": "长网址不能为空"})
        return
    }

    // 通过 Redis 自增生成全局唯一编号
    id, err := redisClient.Incr(ctx, "short_url:id").Result()
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "生成短标识失败"})
        return
    }

    shortKey := encodeBase62(id)
    record := ShortURL{
        LongURL:  longURL,
        ShortKey: shortKey,
    }

    // 先持久化到 MySQL
    if err := db.Create(&record).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "保存映射关系失败"})
        return
    }

    // 再写入 Redis 提升跳转查询速度
    if err := redisClient.Set(ctx, shortKey, longURL, 0).Err(); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "写入缓存失败"})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "short_url": "http://ipipp.com/" + shortKey,
        "long_url":  longURL,
    })
}

// redirectShortURL 根据短标识跳转到原始长网址
func redirectShortURL(c *gin.Context) {
    shortKey := c.Param("short_key")

    longURL, err := redisClient.Get(ctx, shortKey).Result()
    if err == redis.Nil {
        var record ShortURL
        if err := db.Where("short_key = ?", shortKey).First(&record).Error; err != nil {
            c.JSON(http.StatusNotFound, gin.H{"error": "短链接不存在"})
            return
        }
        longURL = record.LongURL
        redisClient.Set(ctx, shortKey, longURL, 0)
    } else if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "查询缓存失败"})
        return
    }

    c.Redirect(http.StatusFound, longURL)
}

func main() {
    if err := initMySQL(); err != nil {
        panic("MySQL 初始化失败: " + err.Error())
    }
    if err := initRedis(); err != nil {
        panic("Redis 初始化失败: " + err.Error())
    }

    router := gin.Default()
    router.POST("/generate", generateShortURL)
    router.GET("/:short_key", redirectShortURL)

    router.Run(":8080")
}

这段实现中有几个值得注意的细节。第一,短标识的唯一性依赖 Redis 的自增操作,因此多个服务实例可以同时生成短链接而不会出现编号冲突。第二,MySQL 中的 ShortKey 字段设置了唯一索引,即使缓存出现异常,数据库层也能阻止重复短标识写入。第三,跳转接口返回的是临时重定向,适合短链接这种中间入口。若业务明确希望浏览器缓存跳转结果,也可以根据场景调整为永久重定向,但需要谨慎评估后续变更成本。

测试验证与生产环境优化方向

服务启动后,可以通过命令行工具验证功能。首先生成短链接,然后访问返回结果中的短路径,观察是否能够跳转到原始地址。以下示例使用本地服务端口进行演示,请求中的长网址可以根据实际环境替换。

# 生成短链接
curl -X POST -d "long_url=https://ipipp.com/help/doc" http://127.0.0.1:8080/generate

# 访问短链接,验证是否跳转
curl -L http://127.0.0.1:8080/abc123

在基础功能跑通之后,还可以围绕稳定性与运营能力继续完善。可以从以下几个方向入手。

  • 过期控制,短链接不一定需要永久有效,可以在 Redis 中设置过期时间,并配合数据库状态字段判断是否允许跳转。
  • 访问统计,记录每个短标识的访问次数、来源和响应状态,便于分析投放效果。
  • 安全校验,服务应对提交的长网址做基本检查,过滤明显非法的地址,避免短链接被用于恶意跳转。
  • 缓存优化,为热点短链接延长缓存时间,为冷门短链接设置较短缓存时间,从而平衡内存占用和命中率。

如果服务需要面对更高并发,还可以进一步优化缓存策略和 ID 分配方式。例如在 Redis 之外引入本地缓存,减少网络往返;在 ID 分配上采用分段自增或分布式 ID 生成策略,降低单点压力。总体而言,短链接服务的核心并不复杂,但要在唯一性、性能、可维护性和安全性之间做好平衡。通过 Golang 搭建基础版本后,再逐步加入统计、限流、过期和审计能力,就可以形成一个适合生产环境使用的短链接服务。

GolangURL短链接短链生成Redis缓存HTTP服务修改时间:2026-07-10 16:00:45

免责声明:​ 已尽一切努力确保本网站所含信息的准确性。网站内容多为原创整理与精心编撰,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们处理。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。