核心目标:通过一个完整的 IM 项目,系统性地掌握 Go 语言后端开发的核心技能——从项目初始化、数据库操作、用户认证到 WebSocket 实时通信。
项目概览
技术栈
| 组件 | 选型 | 理由 |
|---|---|---|
| Web 框架 | Gin | 轻量高效,Go 生态主流 |
| 数据库 | PostgreSQL + pgx | 原生驱动,学习 SQL |
| 配置管理 | Viper | 支持 YAML,功能强大 |
| 认证 | JWT + bcrypt | 无状态认证,安全哈希 |
| 实时通信 | gorilla/websocket | 最成熟的 WebSocket 库 |
项目结构(Go 标准布局)
go-im/
├── cmd/server/main.go # 程序入口
├── config/config.yaml # 配置文件
├── internal/ # 私有代码(外部不可导入)
│ ├── config/ # 配置加载
│ ├── model/ # 数据模型
│ ├── repository/ # 数据访问层
│ ├── service/ # 业务逻辑层
│ ├── handler/ # HTTP 处理层
│ ├── ws/ # WebSocket 相关
│ └── middleware/ # 中间件
├── pkg/ # 公共代码(可被外部导入)
│ ├── database/ # 数据库连接
│ └── utils/ # 工具函数
└── go.mod
知识点:
internal目录是 Go 的约定,该目录下的包只能被当前模块导入,外部模块无法访问。这是一种代码可见性的控制机制。
第一部分:项目初始化与配置管理
1.1 Go Module 初始化
go mod init go-im
这会创建 go.mod 文件,类似 Node.js 的 package.json 或 PHP 的 composer.json。
知识点:Go Module 是 Go 1.11 引入的依赖管理方案。
go.mod记录模块名和依赖版本,go.sum记录依赖的哈希校验值。
1.2 配置加载(Viper)
// internal/config/config.go
package config
import (
"log"
"github.com/spf13/viper"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
JWT JWTConfig `mapstructure:"jwt"`
}
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
}
// 生成数据库连接字符串
func (d DatabaseConfig) DSN() string {
return fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%d",
d.Host, d.User, d.Password, d.DBName, d.Port,
)
}
var C Config // 全局配置实例
func Load(path string) {
viper.SetConfigFile(path)
if err := viper.ReadInConfig(); err != nil {
log.Fatalf("failed to read config: %v", err)
}
if err := viper.Unmarshal(&C); err != nil {
log.Fatalf("failed to unmarshal config: %v", err)
}
}
配置文件 config.yaml:
server:
port: 8080
mode: debug
database:
host: localhost
port: 5432
user: postgres
password: postgres
dbname: go_im
知识点:
mapstructuretag 是 Viper 特有的,用于将 YAML key 映射到 Go struct 字段- Go 的 struct tag 用反引号
`定义,格式为key:"value"var C Config是包级别的全局变量,其他包通过config.C访问
第二部分:数据库连接与 Repository 模式
2.1 PostgreSQL 连接(pgx)
// pkg/database/database.go
package database
import (
"context"
"go-im/internal/config"
"github.com/jackc/pgx/v5"
)
var db *pgx.Conn
func Init() error {
var err error
db, err = pgx.Connect(context.Background(), config.C.Database.DSN())
return err
}
func DB() *pgx.Conn {
return db
}
知识点:
- 小写
db是包私有变量,外部无法访问- 通过
DB()函数暴露连接,这是 Go 的惯用模式(封装 + 访问器)context.Background()是根上下文,用于控制超时和取消
2.2 Repository 模式(数据访问层)
// internal/repository/user_repo.go
package repository
import (
"context"
"go-im/internal/model"
"go-im/pkg/database"
)
type UserRepo struct{}
func (r *UserRepo) CreateUser(ctx context.Context, user *model.User) error {
_, err := database.DB().Exec(ctx,
"INSERT INTO users (username, nickname, password) VALUES ($1, $2, $3)",
user.Username, user.Nickname, user.Password,
)
return err
}
func (r *UserRepo) FindByUsername(ctx context.Context, username string) (*model.User, error) {
var user model.User
err := database.DB().QueryRow(ctx,
"SELECT id, username, nickname, password FROM users WHERE username = $1",
username,
).Scan(&user.ID, &user.Username, &user.Nickname, &user.Password)
if err != nil {
return nil, err
}
return &user, nil
}
知识点:
- Repository 模式将数据库操作与业务逻辑分离
$1, $2, $3是 PostgreSQL 的参数占位符,防止 SQL 注入QueryRow返回单行,Scan将结果映射到 struct 字段- 返回
*model.User(指针)避免大结构体的值拷贝
第三部分:Service 层与密码安全
3.1 业务逻辑层
// internal/service/auth.go
package service
import (
"context"
"errors"
"go-im/internal/model"
"go-im/internal/repository"
"golang.org/x/crypto/bcrypt"
)
type AuthService struct{}
func (s *AuthService) Register(ctx context.Context, username, nickname, password string) error {
// 密码哈希
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return errors.New("密码哈希失败")
}
return repository.UserRepo{}.CreateUser(ctx, &model.User{
Username: username,
Nickname: nickname,
Password: string(hash),
})
}
func (s *AuthService) Login(ctx context.Context, username, password string) (*model.User, error) {
user, err := repository.UserRepo{}.FindByUsername(ctx, username)
if err != nil {
return nil, errors.New("用户不存在")
}
// 校验密码
err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password))
if err != nil {
return nil, errors.New("密码错误")
}
return user, nil
}
知识点:
- bcrypt 是专门用于密码哈希的算法,自带盐值(salt),每次哈希结果都不同
GenerateFromPassword生成哈希,CompareHashAndPassword校验密码- 密码永远不能明文存储,这是安全开发的基本原则
- Service 层不直接操作数据库,而是调用 Repository 层
第四部分:Handler 层与 Gin 框架
4.1 请求绑定与参数校验
// internal/handler/auth.go
package handler
import (
"go-im/internal/service"
"go-im/pkg/utils"
"github.com/gin-gonic/gin"
)
type RegisterRequest struct {
Username string `json:"username" binding:"required"`
Nickname string `json:"nickname" binding:"required"`
Password string `json:"password" binding:"required"`
}
type AuthHandler struct {
authService *service.AuthService
}
func NewAuthHandler(s *service.AuthService) *AuthHandler {
return &AuthHandler{authService: s}
}
func (h *AuthHandler) Register() gin.HandlerFunc {
return func(c *gin.Context) {
var req RegisterRequest
// 自动绑定 JSON 并校验
if err := c.ShouldBindJSON(&req); err != nil {
utils.Error(c, 400, "参数错误")
return
}
err := h.authService.Register(c.Request.Context(), req.Username, req.Nickname, req.Password)
if err != nil {
utils.Error(c, 500, "注册失败")
return
}
utils.Success(c, nil)
}
}
知识点:
binding:"required"是 Gin 的校验 tag,字段为空时自动报错ShouldBindJSON自动解析请求体 JSON 到 structgin.HandlerFunc是 Gin 要求的 handler 签名:func(*gin.Context)- 用闭包(return func)实现依赖注入,handler 内部可以访问
h.authService
4.2 统一响应格式
// pkg/utils/response.go
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
func Success(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{Code: 0, Message: "ok", Data: data})
}
func Error(c *gin.Context, httpCode int, msg string) {
c.JSON(httpCode, Response{Code: -1, Message: msg})
}
知识点:
interface{}是 Go 的空接口,任何类型都实现了它,类似 PHP 的mixedomitemptytag 表示字段为空时 JSON 中省略该字段- 统一响应格式让前端处理更简单
4.3 路由分组
// cmd/server/main.go
r := gin.Default()
api := r.Group("/api")
{
authHandler := handler.NewAuthHandler(&service.AuthService{})
api.POST("/register", authHandler.Register())
api.POST("/login", authHandler.Login())
}
知识点:
Group创建路由组,统一添加前缀,适合组织 API 版本(/api/v1/)- Gin 的中间件可以挂在 Group 上,组内所有路由都会经过
第五部分:JWT 认证
5.1 JWT 生成与解析
// pkg/utils/jwt.go
package utils
import (
"time"
"github.com/golang-jwt/jwt/v5"
"go-im/internal/config"
)
func GenerateToken(userID int) (string, error) {
claims := jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(time.Duration(config.C.JWT.ExpireHours) * time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(config.C.JWT.Secret))
}
func ParseToken(tokenStr string) (int, error) {
token, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
return []byte(config.C.JWT.Secret), nil
})
if err != nil {
return 0, err
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok || !token.Valid {
return 0, errors.New("invalid token")
}
userID := int(claims["user_id"].(float64))
return userID, nil
}
5.2 认证中间件
// internal/middleware/auth.go
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
token := c.GetHeader("Authorization")
if token == "" {
utils.Error(c, 401, "未登录")
c.Abort()
return
}
// 去掉 "Bearer " 前缀
token = strings.TrimPrefix(token, "Bearer ")
userID, err := utils.ParseToken(token)
if err != nil {
utils.Error(c, 401, "token 无效")
c.Abort()
return
}
// 将 userID 存入上下文,后续 handler 可以获取
c.Set("user_id", userID)
c.Next()
}
}
知识点:
- JWT(JSON Web Token)是无状态认证方案,服务端不需要存储 session
c.Abort()终止后续 handler 执行c.Set("user_id", userID)将数据存入 Gin 的上下文,后续通过c.Get("user_id")获取c.Next()放行请求,继续执行后续中间件/handler
第六部分:WSL2 网络问题
在 WSL2 中开发时,Windows 宿主机无法直接访问 WSL 内的服务。
原因
WSL2 默认使用 NAT 模式,Linux 有独立的 IP 地址。
解决方案
快速方案:使用 WSL 的 IP 地址访问
# 查看 WSL IP
ip addr show eth0 | grep inet
# 输出类似:inet 192.168.64.33/20
# Windows 浏览器访问
http://192.168.64.33:8080/ping
一劳永逸:开启镜像网络
# C:\Users\用户名\.wslconfig
[wsl2]
networkingMode=mirrored
然后 PowerShell 执行 wsl --shutdown,重启后 localhost 直通。
知识点速查表
| 概念 | 说明 |
|---|---|
go mod init |
初始化模块,类似 npm init |
internal/ |
包私有目录,外部不可导入 |
mapstructure |
Viper 的 tag,YAML → struct 映射 |
binding:"required" |
Gin 的请求校验 tag |
ShouldBindJSON |
Gin 自动解析 JSON 请求体 |
gin.HandlerFunc |
Gin handler 的标准签名 |
bcrypt |
密码哈希算法,自带盐值 |
| JWT | 无状态认证,服务端不存 session |
c.Set / c.Get |
Gin 上下文存取数据 |
c.Abort() |
终止请求链 |
$1, $2 |
PostgreSQL 参数占位符,防 SQL 注入 |
*T (指针) |
避免值拷贝,可修改原数据 |
interface{} |
空接口,任意类型 |
下一步计划
- WebSocket 长连接 + Hub 模式
- 单聊(点对点消息)
- 群聊 + 消息广播
- 消息持久化 + 离线消息
- 在线状态 + 心跳检测