Go语言快速入门Go语言快速入门
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
  • 实战篇

    • 🚀 实战篇
    • 第1个项目 - 命令行文件管理器
    • 第2个项目 - RESTful API Todo服务
    • 第3个项目 - Web爬虫 新闻采集器
    • 第4个项目 - 实时聊天室 WebSocket
    • 第5个项目 - URL短链接服务
    • 第6个项目 - 完整博客系统

第5个项目 - URL短链接服务

嘿,朋友们!我是长安。

今天我们要做的第五个实战项目是URL短链接服务。这是一个类似于bit.ly的服务,可以将长URL转换为短链接,方便分享和统计。

说实话,短链接服务看似简单,但涉及很多技术要点:分布式ID生成、Redis缓存、高并发处理、数据统计等。我当年面试的时候被问到过这个问题,如何设计一个短链接系统。这是一个典型的高并发场景实战!

🎯 项目目标

实现一个完整的短链接服务,支持以下功能:

功能说明技术点
生成短链接将长URL转换为短码Base62编码、雪花算法
链接跳转短链接重定向到原始URLHTTP重定向、缓存
访问统计记录访问次数、来源Redis计数器
链接管理查看、删除自己的链接CRUD操作
过期时间设置链接有效期TTL管理

📁 项目结构

url-shortener/
├── main.go              # 程序入口
├── config/
│   └── config.go       # 配置管理
├── models/
│   └── url.go          # URL数据模型
├── services/
│   ├── shortener.go    # 短链接服务
│   └── snowflake.go    # ID生成器
├── handlers/
│   └── url.go          # HTTP处理器
├── storage/
│   ├── mysql.go        # MySQL存储
│   └── redis.go        # Redis缓存
├── middleware/
│   └── ratelimit.go    # 限流中间件
└── go.mod

🚀 第一步:项目初始化

# 创建项目目录
mkdir url-shortener
cd url-shortener

# 初始化Go模块
go mod init github.com/yourusername/url-shortener

# 安装依赖
go get -u github.com/gin-gonic/gin
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql
go get -u github.com/go-redis/redis/v8
go get -u github.com/bwmarrin/snowflake

# 创建目录结构
mkdir config models services handlers storage middleware

📦 第二步:配置管理

文件: config/config.go

package config

import (
	"os"
)

// Config 应用配置
type Config struct {
	Server   ServerConfig
	MySQL    MySQLConfig
	Redis    RedisConfig
	Domain   string // 短链接域名
	CodeLen  int    // 短码长度
}

type ServerConfig struct {
	Port string
}

type MySQLConfig struct {
	Host     string
	Port     string
	User     string
	Password string
	Database string
}

type RedisConfig struct {
	Host     string
	Port     string
	Password string
	DB       int
}

// LoadConfig 加载配置
func LoadConfig() *Config {
	return &Config{
		Server: ServerConfig{
			Port: getEnv("SERVER_PORT", "8080"),
		},
		MySQL: MySQLConfig{
			Host:     getEnv("MYSQL_HOST", "localhost"),
			Port:     getEnv("MYSQL_PORT", "3306"),
			User:     getEnv("MYSQL_USER", "root"),
			Password: getEnv("MYSQL_PASSWORD", ""),
			Database: getEnv("MYSQL_DATABASE", "url_shortener"),
		},
		Redis: RedisConfig{
			Host:     getEnv("REDIS_HOST", "localhost"),
			Port:     getEnv("REDIS_PORT", "6379"),
			Password: getEnv("REDIS_PASSWORD", ""),
			DB:       0,
		},
		Domain:  getEnv("DOMAIN", "http://localhost:8080"),
		CodeLen: 6,
	}
}

func getEnv(key, defaultValue string) string {
	if value := os.Getenv(key); value != "" {
		return value
	}
	return defaultValue
}

📊 第三步:数据模型

文件: models/url.go

package models

import (
	"time"

	"gorm.io/gorm"
)

// URL 短链接模型
type URL struct {
	ID          uint           `gorm:"primaryKey" json:"id"`
	ShortCode   string         `gorm:"uniqueIndex;size:10" json:"short_code"`
	OriginalURL string         `gorm:"type:text;not null" json:"original_url"`
	Clicks      int64          `gorm:"default:0" json:"clicks"`
	ExpiresAt   *time.Time     `json:"expires_at,omitempty"`
	CreatedAt   time.Time      `json:"created_at"`
	UpdatedAt   time.Time      `json:"updated_at"`
	DeletedAt   gorm.DeletedAt `gorm:"index" json:"-"`
}

// CreateURLRequest 创建短链接请求
type CreateURLRequest struct {
	URL       string `json:"url" binding:"required,url"`
	ExpiresIn int    `json:"expires_in"` // 过期时间(秒),0表示永不过期
}

// CreateURLResponse 创建短链接响应
type CreateURLResponse struct {
	ShortCode string `json:"short_code"`
	ShortURL  string `json:"short_url"`
	LongURL   string `json:"long_url"`
	ExpiresAt string `json:"expires_at,omitempty"`
}

// URLStats 链接统计
type URLStats struct {
	ShortCode   string    `json:"short_code"`
	ShortURL    string    `json:"short_url"`
	OriginalURL string    `json:"original_url"`
	Clicks      int64     `json:"clicks"`
	CreatedAt   time.Time `json:"created_at"`
	ExpiresAt   string    `json:"expires_at,omitempty"`
}

// IsExpired 检查是否过期
func (u *URL) IsExpired() bool {
	if u.ExpiresAt == nil {
		return false
	}
	return time.Now().After(*u.ExpiresAt)
}

🔢 第四步:ID生成器(雪花算法)

文件: services/snowflake.go

package services

import (
	"github.com/bwmarrin/snowflake"
	"log"
)

var node *snowflake.Node

// InitSnowflake 初始化雪花算法
func InitSnowflake(nodeID int64) error {
	var err error
	node, err = snowflake.NewNode(nodeID)
	if err != nil {
		return err
	}
	log.Printf("雪花算法初始化成功, Node ID: %d", nodeID)
	return nil
}

// GenerateID 生成唯一ID
func GenerateID() int64 {
	return node.Generate().Int64()
}

🔗 第五步:短链接服务

文件: services/shortener.go

package services

import (
	"fmt"
	"math"
	"strings"
)

const (
	// Base62字符集
	base62Chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
)

// Base62Encode 将数字编码为Base62
func Base62Encode(num int64) string {
	if num == 0 {
		return string(base62Chars[0])
	}

	var result strings.Builder
	base := int64(len(base62Chars))

	for num > 0 {
		remainder := num % base
		result.WriteByte(base62Chars[remainder])
		num = num / base
	}

	// 反转字符串
	encoded := result.String()
	runes := []rune(encoded)
	for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
		runes[i], runes[j] = runes[j], runes[i]
	}

	return string(runes)
}

// Base62Decode 将Base62解码为数字
func Base62Decode(encoded string) (int64, error) {
	var num int64
	base := int64(len(base62Chars))

	for i, char := range encoded {
		pos := strings.IndexRune(base62Chars, char)
		if pos == -1 {
			return 0, fmt.Errorf("invalid character: %c", char)
		}

		power := len(encoded) - i - 1
		num += int64(pos) * int64(math.Pow(float64(base), float64(power)))
	}

	return num, nil
}

// GenerateShortCode 生成短码
func GenerateShortCode() string {
	id := GenerateID()
	return Base62Encode(id)
}

💾 第六步:存储层

文件: storage/mysql.go

package storage

import (
	"fmt"
	"time"

	"github.com/yourusername/url-shortener/config"
	"github.com/yourusername/url-shortener/models"
	"gorm.io/driver/mysql"
	"gorm.io/gorm"
	"gorm.io/gorm/logger"
)

type MySQLStorage struct {
	db *gorm.DB
}

// NewMySQLStorage 创建MySQL存储
func NewMySQLStorage(cfg config.MySQLConfig) (*MySQLStorage, error) {
	dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
		cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)

	db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
		Logger: logger.Default.LogMode(logger.Info),
	})
	if err != nil {
		return nil, err
	}

	// 自动迁移
	if err := db.AutoMigrate(&models.URL{}); err != nil {
		return nil, err
	}

	return &MySQLStorage{db: db}, nil
}

// Create 创建短链接
func (s *MySQLStorage) Create(url *models.URL) error {
	return s.db.Create(url).Error
}

// GetByShortCode 根据短码查询
func (s *MySQLStorage) GetByShortCode(code string) (*models.URL, error) {
	var url models.URL
	err := s.db.Where("short_code = ?", code).First(&url).Error
	return &url, err
}

// GetByOriginalURL 根据原始URL查询
func (s *MySQLStorage) GetByOriginalURL(originalURL string) (*models.URL, error) {
	var url models.URL
	err := s.db.Where("original_url = ?", originalURL).First(&url).Error
	return &url, err
}

// IncrementClicks 增加点击次数
func (s *MySQLStorage) IncrementClicks(code string) error {
	return s.db.Model(&models.URL{}).
		Where("short_code = ?", code).
		UpdateColumn("clicks", gorm.Expr("clicks + ?", 1)).
		Error
}

// Delete 删除短链接
func (s *MySQLStorage) Delete(code string) error {
	return s.db.Where("short_code = ?", code).Delete(&models.URL{}).Error
}

// GetAll 获取所有链接(分页)
func (s *MySQLStorage) GetAll(page, pageSize int) ([]models.URL, int64, error) {
	var urls []models.URL
	var total int64

	offset := (page - 1) * pageSize

	if err := s.db.Model(&models.URL{}).Count(&total).Error; err != nil {
		return nil, 0, err
	}

	err := s.db.Offset(offset).Limit(pageSize).
		Order("created_at desc").
		Find(&urls).Error

	return urls, total, err
}

文件: storage/redis.go

package storage

import (
	"context"
	"encoding/json"
	"fmt"
	"time"

	"github.com/go-redis/redis/v8"
	"github.com/yourusername/url-shortener/config"
	"github.com/yourusername/url-shortener/models"
)

type RedisStorage struct {
	client *redis.Client
	ctx    context.Context
}

// NewRedisStorage 创建Redis存储
func NewRedisStorage(cfg config.RedisConfig) *RedisStorage {
	client := redis.NewClient(&redis.Options{
		Addr:     fmt.Sprintf("%s:%s", cfg.Host, cfg.Port),
		Password: cfg.Password,
		DB:       cfg.DB,
	})

	ctx := context.Background()

	// 测试连接
	if err := client.Ping(ctx).Err(); err != nil {
		panic(fmt.Sprintf("Redis连接失败: %v", err))
	}

	return &RedisStorage{
		client: client,
		ctx:    ctx,
	}
}

// Set 缓存URL
func (r *RedisStorage) Set(code string, url *models.URL, expiration time.Duration) error {
	data, err := json.Marshal(url)
	if err != nil {
		return err
	}

	key := fmt.Sprintf("url:%s", code)
	return r.client.Set(r.ctx, key, data, expiration).Err()
}

// Get 获取缓存的URL
func (r *RedisStorage) Get(code string) (*models.URL, error) {
	key := fmt.Sprintf("url:%s", code)
	data, err := r.client.Get(r.ctx, key).Bytes()
	if err != nil {
		return nil, err
	}

	var url models.URL
	if err := json.Unmarshal(data, &url); err != nil {
		return nil, err
	}

	return &url, nil
}

// Delete 删除缓存
func (r *RedisStorage) Delete(code string) error {
	key := fmt.Sprintf("url:%s", code)
	return r.client.Del(r.ctx, key).Err()
}

// IncrementClicks 增加点击计数
func (r *RedisStorage) IncrementClicks(code string) error {
	key := fmt.Sprintf("clicks:%s", code)
	return r.client.Incr(r.ctx, key).Err()
}

// GetClicks 获取点击次数
func (r *RedisStorage) GetClicks(code string) (int64, error) {
	key := fmt.Sprintf("clicks:%s", code)
	return r.client.Get(r.ctx, key).Int64()
}

🎮 第七步:HTTP处理器

文件: handlers/url.go

package handlers

import (
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/url-shortener/config"
	"github.com/yourusername/url-shortener/models"
	"github.com/yourusername/url-shortener/services"
	"github.com/yourusername/url-shortener/storage"
	"gorm.io/gorm"
)

type URLHandler struct {
	mysql  *storage.MySQLStorage
	redis  *storage.RedisStorage
	config *config.Config
}

func NewURLHandler(mysql *storage.MySQLStorage, redis *storage.RedisStorage, cfg *config.Config) *URLHandler {
	return &URLHandler{
		mysql:  mysql,
		redis:  redis,
		config: cfg,
	}
}

// CreateShortURL 创建短链接
// POST /api/shorten
func (h *URLHandler) CreateShortURL(c *gin.Context) {
	var req models.CreateURLRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{
			"error": "参数错误: " + err.Error(),
		})
		return
	}

	// 检查URL是否已存在
	existingURL, err := h.mysql.GetByOriginalURL(req.URL)
	if err == nil && !existingURL.IsExpired() {
		// URL已存在且未过期,直接返回
		c.JSON(http.StatusOK, models.CreateURLResponse{
			ShortCode: existingURL.ShortCode,
			ShortURL:  h.config.Domain + "/" + existingURL.ShortCode,
			LongURL:   existingURL.OriginalURL,
		})
		return
	}

	// 生成短码
	shortCode := services.GenerateShortCode()

	// 创建URL记录
	url := &models.URL{
		ShortCode:   shortCode,
		OriginalURL: req.URL,
	}

	// 设置过期时间
	if req.ExpiresIn > 0 {
		expiresAt := time.Now().Add(time.Duration(req.ExpiresIn) * time.Second)
		url.ExpiresAt = &expiresAt
	}

	// 保存到数据库
	if err := h.mysql.Create(url); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": "创建失败",
		})
		return
	}

	// 缓存到Redis
	cacheDuration := 24 * time.Hour
	if url.ExpiresAt != nil {
		cacheDuration = time.Until(*url.ExpiresAt)
	}
	h.redis.Set(shortCode, url, cacheDuration)

	// 返回响应
	response := models.CreateURLResponse{
		ShortCode: shortCode,
		ShortURL:  h.config.Domain + "/" + shortCode,
		LongURL:   url.OriginalURL,
	}

	if url.ExpiresAt != nil {
		response.ExpiresAt = url.ExpiresAt.Format(time.RFC3339)
	}

	c.JSON(http.StatusCreated, response)
}

// Redirect 短链接跳转
// GET /:code
func (h *URLHandler) Redirect(c *gin.Context) {
	code := c.Param("code")

	// 先从Redis获取
	url, err := h.redis.Get(code)
	if err != nil {
		// Redis未命中,从MySQL获取
		url, err = h.mysql.GetByShortCode(code)
		if err != nil {
			if err == gorm.ErrRecordNotFound {
				c.JSON(http.StatusNotFound, gin.H{
					"error": "短链接不存在",
				})
			} else {
				c.JSON(http.StatusInternalServerError, gin.H{
					"error": "查询失败",
				})
			}
			return
		}

		// 缓存到Redis
		cacheDuration := 24 * time.Hour
		if url.ExpiresAt != nil {
			cacheDuration = time.Until(*url.ExpiresAt)
		}
		h.redis.Set(code, url, cacheDuration)
	}

	// 检查是否过期
	if url.IsExpired() {
		c.JSON(http.StatusGone, gin.H{
			"error": "短链接已过期",
		})
		return
	}

	// 异步增加点击次数
	go func() {
		h.redis.IncrementClicks(code)
		h.mysql.IncrementClicks(code)
	}()

	// 重定向
	c.Redirect(http.StatusMovedPermanently, url.OriginalURL)
}

// GetStats 获取链接统计
// GET /api/stats/:code
func (h *URLHandler) GetStats(c *gin.Context) {
	code := c.Param("code")

	url, err := h.mysql.GetByShortCode(code)
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			c.JSON(http.StatusNotFound, gin.H{
				"error": "短链接不存在",
			})
		} else {
			c.JSON(http.StatusInternalServerError, gin.H{
				"error": "查询失败",
			})
		}
		return
	}

	stats := models.URLStats{
		ShortCode:   url.ShortCode,
		ShortURL:    h.config.Domain + "/" + url.ShortCode,
		OriginalURL: url.OriginalURL,
		Clicks:      url.Clicks,
		CreatedAt:   url.CreatedAt,
	}

	if url.ExpiresAt != nil {
		stats.ExpiresAt = url.ExpiresAt.Format(time.RFC3339)
	}

	c.JSON(http.StatusOK, stats)
}

// GetAllURLs 获取所有链接
// GET /api/urls?page=1&page_size=10
func (h *URLHandler) GetAllURLs(c *gin.Context) {
	page := 1
	pageSize := 10

	if p, ok := c.GetQuery("page"); ok {
		if parsed, err := time.ParseDuration(p); err == nil {
			page = int(parsed)
		}
	}

	if ps, ok := c.GetQuery("page_size"); ok {
		if parsed, err := time.ParseDuration(ps); err == nil {
			pageSize = int(parsed)
		}
	}

	urls, total, err := h.mysql.GetAll(page, pageSize)
	if err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": "查询失败",
		})
		return
	}

	c.JSON(http.StatusOK, gin.H{
		"data":       urls,
		"total":      total,
		"page":       page,
		"page_size":  pageSize,
		"total_page": (total + int64(pageSize) - 1) / int64(pageSize),
	})
}

// DeleteURL 删除短链接
// DELETE /api/urls/:code
func (h *URLHandler) DeleteURL(c *gin.Context) {
	code := c.Param("code")

	// 从MySQL删除
	if err := h.mysql.Delete(code); err != nil {
		c.JSON(http.StatusInternalServerError, gin.H{
			"error": "删除失败",
		})
		return
	}

	// 从Redis删除
	h.redis.Delete(code)

	c.JSON(http.StatusOK, gin.H{
		"message": "删除成功",
	})
}

🚦 第八步:限流中间件

文件: middleware/ratelimit.go

package middleware

import (
	"net/http"
	"sync"
	"time"

	"github.com/gin-gonic/gin"
)

// RateLimiter 简单的限流器
type RateLimiter struct {
	requests map[string]*requestInfo
	mu       sync.Mutex
	limit    int           // 限制请求数
	window   time.Duration // 时间窗口
}

type requestInfo struct {
	count     int
	resetTime time.Time
}

// NewRateLimiter 创建限流器
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
	rl := &RateLimiter{
		requests: make(map[string]*requestInfo),
		limit:    limit,
		window:   window,
	}

	// 定期清理过期记录
	go rl.cleanup()

	return rl
}

// Middleware 限流中间件
func (rl *RateLimiter) Middleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		ip := c.ClientIP()

		rl.mu.Lock()
		defer rl.mu.Unlock()

		now := time.Now()
		info, exists := rl.requests[ip]

		if !exists || now.After(info.resetTime) {
			// 新的时间窗口
			rl.requests[ip] = &requestInfo{
				count:     1,
				resetTime: now.Add(rl.window),
			}
			c.Next()
			return
		}

		if info.count >= rl.limit {
			// 超过限制
			c.JSON(http.StatusTooManyRequests, gin.H{
				"error": "请求过于频繁,请稍后再试",
			})
			c.Abort()
			return
		}

		info.count++
		c.Next()
	}
}

// cleanup 清理过期记录
func (rl *RateLimiter) cleanup() {
	ticker := time.NewTicker(time.Minute)
	defer ticker.Stop()

	for range ticker.C {
		rl.mu.Lock()
		now := time.Now()
		for ip, info := range rl.requests {
			if now.After(info.resetTime) {
				delete(rl.requests, ip)
			}
		}
		rl.mu.Unlock()
	}
}

🌐 第九步:主程序

文件: main.go

package main

import (
	"log"

	"github.com/gin-gonic/gin"
	"github.com/yourusername/url-shortener/config"
	"github.com/yourusername/url-shortener/handlers"
	"github.com/yourusername/url-shortener/middleware"
	"github.com/yourusername/url-shortener/services"
	"github.com/yourusername/url-shortener/storage"
	"time"
)

func main() {
	// 加载配置
	cfg := config.LoadConfig()

	// 初始化雪花算法
	if err := services.InitSnowflake(1); err != nil {
		log.Fatalf("初始化雪花算法失败: %v", err)
	}

	// 初始化MySQL
	mysql, err := storage.NewMySQLStorage(cfg.MySQL)
	if err != nil {
		log.Fatalf("连接MySQL失败: %v", err)
	}
	log.Println("✅ MySQL连接成功")

	// 初始化Redis
	redis := storage.NewRedisStorage(cfg.Redis)
	log.Println("✅ Redis连接成功")

	// 创建处理器
	urlHandler := handlers.NewURLHandler(mysql, redis, cfg)

	// 创建Gin引擎
	router := gin.Default()

	// 限流中间件(每分钟100次请求)
	rateLimiter := middleware.NewRateLimiter(100, time.Minute)
	router.Use(rateLimiter.Middleware())

	// API路由
	api := router.Group("/api")
	{
		api.POST("/shorten", urlHandler.CreateShortURL)
		api.GET("/stats/:code", urlHandler.GetStats)
		api.GET("/urls", urlHandler.GetAllURLs)
		api.DELETE("/urls/:code", urlHandler.DeleteURL)
	}

	// 短链接跳转
	router.GET("/:code", urlHandler.Redirect)

	// 健康检查
	router.GET("/health", func(c *gin.Context) {
		c.JSON(200, gin.H{
			"status": "ok",
		})
	})

	// 启动服务器
	log.Printf("🚀 服务器启动在 http://localhost:%s", cfg.Server.Port)
	if err := router.Run(":" + cfg.Server.Port); err != nil {
		log.Fatal(err)
	}
}

🧪 第十步:测试

1. 准备环境

# 启动MySQL
docker run -d --name mysql \
  -e MYSQL_ROOT_PASSWORD=root \
  -e MYSQL_DATABASE=url_shortener \
  -p 3306:3306 \
  mysql:8.0

# 启动Redis
docker run -d --name redis \
  -p 6379:6379 \
  redis:7

# 或者直接安装MySQL和Redis

2. 运行程序

go run main.go

3. 测试API

# 1. 创建短链接
curl -X POST http://localhost:8080/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"url":"https://github.com/golang/go"}'

# 响应:
# {
#   "short_code": "a1B2c3",
#   "short_url": "http://localhost:8080/a1B2c3",
#   "long_url": "https://github.com/golang/go"
# }

# 2. 访问短链接
curl -L http://localhost:8080/a1B2c3

# 3. 查看统计
curl http://localhost:8080/api/stats/a1B2c3

# 4. 创建带过期时间的短链接(7天)
curl -X POST http://localhost:8080/api/shorten \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com","expires_in":604800}'

# 5. 删除短链接
curl -X DELETE http://localhost:8080/api/urls/a1B2c3

💡 扩展思考

1. 自定义短码

允许用户自定义短码:

type CreateURLRequest struct {
	URL        string `json:"url" binding:"required,url"`
	CustomCode string `json:"custom_code"`
	ExpiresIn  int    `json:"expires_in"`
}

2. 访问分析

记录更详细的访问信息:

type ClickLog struct {
	URLCode   string
	IP        string
	UserAgent string
	Referer   string
	CreatedAt time.Time
}

3. QR码生成

为短链接生成二维码:

go get -u github.com/skip2/go-qrcode
import "github.com/skip2/go-qrcode"

func GenerateQRCode(url string) ([]byte, error) {
	return qrcode.Encode(url, qrcode.Medium, 256)
}

4. 链接预览

抓取原始URL的标题和描述:

import "github.com/PuerkitoBio/goquery"

func FetchMetadata(url string) (*Metadata, error) {
	// 抓取并解析HTML
	// 提取title, description, og:image等
}

💪 练习题

  1. 实现短链接的访问密码保护功能
  2. 添加链接分类和标签功能
  3. 实现短链接的批量导入导出
  4. 添加链接的访问地理位置统计
💡 参考答案
// 密码保护示例
type URL struct {
	// ... existing fields ...
	Password string `json:"-"` // 访问密码
}

func (h *URLHandler) Redirect(c *gin.Context) {
	code := c.Param("code")
	password := c.Query("password")
	
	url, err := h.mysql.GetByShortCode(code)
	if err != nil {
		c.JSON(404, gin.H{"error": "链接不存在"})
		return
	}
	
	// 检查密码
	if url.Password != "" && url.Password != password {
		c.JSON(401, gin.H{"error": "密码错误"})
		return
	}
	
	// 继续跳转...
}

🎯 小结

恭喜你完成了第五个实战项目!通过这个项目,你学会了:

✅ 分布式ID生成(雪花算法)
✅ Base62编码算法
✅ Redis缓存策略
✅ MySQL数据持久化
✅ 高并发场景处理
✅ 限流中间件实现

下一个项目是最后一个综合项目——完整的博客系统!

下一步: 第6个项目 - 完整博客系统 →


💬 遇到问题了吗?

  • 确保MySQL和Redis已正确安装
  • 检查数据库连接配置
  • 注意并发安全问题
  • 访问 GORM文档 了解更多
最近更新: 2025/12/27 13:26
Contributors: 王长安
Prev
第4个项目 - 实时聊天室 WebSocket
Next
第6个项目 - 完整博客系统