Go-web实例

转换文字版(供搜索 / 复制)
这份资源清单是专门为新手挑选的。我按照学习顺序和难易程度进行了分类,重点推荐了中文社区最优质的教程(特别是李文周的博客,被称为 Go 语言新手的“圣经”)。
1. HTTP 与网络基础 (必看理论)
在写代码前,你需要先搞懂“浏览器和服务器是怎么对话的”。
- MDN HTTP 概述 (权威文档)
- MDN Web Docs - HTTP
- 推荐理由:Mozilla 的官方文档,全球公认最权威的前端/Web文档,内容非常详细。
- 图解 HTTP (通俗易懂)
- 阮一峰:HTTP 协议入门
- 推荐理由:阮一峰老师的文章深入浅出,适合初学者快速理解概念。
2. Go 语言原生 Web 开发 (net/http)
不要跳过这一步直接学 Gin,理解原生库才能看懂框架在做什么。
- Go Web 编程 (开源书)
- Build Web Application with Golang - HTTP 基础
- 推荐理由:Go 语言早期的经典开源书籍,第3章专门讲 HTTP 和 Go 的原生实现,非常经典。
- Go By Example (代码示例)
- HTTP Servers (服务端)
- HTTP Clients (客户端)
- 推荐理由:没有废话,直接给你看代码怎么写,适合复制粘贴运行体验。
3. Gin 框架 (核心重点)
这是你目前最需要掌握的工具。
- 李文周 (Q1mi) 的 Gin 框架教程 (⭐ 强烈推荐)
- Gin框架介绍及使用
- 推荐理由:这是目前国内写得最好的 Go Web 入门系列。请把他的 Gin 系列文章从头读到尾,你的作业基本都能在这里找到答案。
- Gin 官方文档
- Gin Web Framework
- 推荐理由:Gin 的中文文档不仅翻译得好,而且有很多现成的例子(比如怎么上传文件、怎么分组路由),适合当字典查。
4. 身份校验 (Cookie/Session/JWT)
理解如何给用户“发身份证”。
- JWT 官网 (调试工具)
- jwt.io
- 推荐理由:这不仅是文档,还是一个调试器。你可以把生成的 Token 粘贴进去,看看里面到底存了什么数据,非常直观。
- Go 语言中使用 JWT (实战)
- 在 Gin 中使用 JWT (李文周)
- 推荐理由:手把手教你如何安装 jwt-go 库,如何生成 Token,以及如何写中间件解析 Token。Lv3 的作业直接参考这篇。
5. Hertz 框架 (字节跳动)
学完 Gin 后,看这个会非常快。
- CloudWeGo 官方文档
- Hertz 概览
- 推荐理由:官方文档写得很清楚,特别是如果你将来想进字节跳动或者使用微服务,这个框架是必学的。
6. 项目结构与分层 (工程化)
如何摆放你的代码文件(Model-Dao-Api)。
- Go 项目分层结构设计
- Go Web 项目结构 (参考)
- (注:初学者不要追求太复杂的 DDD 架构,只需看简单的分层即可)
- 简单理解:可以参考 "CLD 分层" (Controller/Api -> Logic/Service -> Dao) 的相关文章。
💡 视频教程推荐 (Bilibili)
如果你更喜欢看视频操作:
- 七米 (李文周):B站搜索“七米”,他的 Gin 框架视频教程和他的博客一样优秀。
- 极客兔兔 (Geektutu):搜索“7天用Go动手写/从零实现Web框架”,这个系列会带你从零造一个 Gin 框架,能让你彻底理解框架内部原理(适合 LvX 进阶)。
🚀 学习路线建议
- 先看 Go By Example 把 net/http 的 Hello World 跑通。
- 重点攻读 李文周的 Gin 教程,完成作业的 Lv0 - Lv2。
- 去 jwt.io 玩一下 Token,然后照着 李文周的 JWT 教程 完成 Lv3 - Lv4。
- 最后看 Go Web 编程 补充理论细节。
- 李文周的博客 | 总结Go语言学习之路,提供免费的Go语言学习教程,希望与大家一起学习进步。
package utils
//发放通行证
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
var mySecret = []byte("lao-tou-de-mi-yao") // 您的私房密钥
// 发证
func GenerateToken(username string) (string, error) { //参数为用户名,为他发放
claims := jwt.MapClaims{ //原始的钥匙
"username": username,
"exp": time.Now().Add(time.Hour * 2).Unix(), // 2小时过期(钥匙必须声明过期时间)
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) //利用算法包装原始钥匙为未签名的钥匙
return token.SignedString(mySecret) //将包装结果打造成密钥(用我的私房密钥签名)
}
// 提取验证(解析 Token)
func ParseToken(tokenString string) (string, error) { //传入密钥
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { //将密钥转换为*jwt.Token对象,即未签名的钥匙
return mySecret, nil
})
//转换后实现判断
if claims /*ok为true则claims赋值为有效的jwt.MapClaims类型*/, ok /*ok为断言结果*/ := token.Claims.(jwt.MapClaims); /*对token.Claims做类型断言,判断是否为jwt.MapClaims类型*/
ok /*先判断ok再判断token是否过期*/ && token.Valid {
return claims["username"].(string), nil //成功后返回密钥原本数据中的“username”其中.(string)为断言string类型,因为claims为MapClaims类型,他的值为inerface{}
}
return "", err
}
package model
// User 用户:画廊的会员
type User struct {
Username string `json:"username"`
Password string `json:"password"`
}
// Work 作品:挂在墙上的画
type Work struct {
Title string `json:"title"` // 作品标题
Content string `json:"content"` // 作品内容(或者是图片链接)
Author string `json:"author"` // 作者是谁(重点!)
}
package middleware
import (
"painting/utils"
"strings"
"github.com/gin-gonic/gin"
)
//用context而不是string或channel的原因是
/*
1.string为纯文本,而请求链路中有多种类型数据(用户TOKEN,请求参数,响应状态),也无法实现请求
2.channel控制不了流程,要控制生命周期
3.context则是一站式数据传递与控制需求
*/
// 接受并处理HTTP请求
func AuthMiddleware() gin.HandlerFunc /*本质就是func(*gin.Context)*/ { // AuthMiddleware()负责创建func(c *gin.Context)来对HTTP请求进行查验
return func(c *gin.Context) {
// 1. 从请求头里拿 Authorization: Bearer xxxxx
authHeader := c.GetHeader("Authorization") //获取请求头
if authHeader == "" { //为空
c.AbortWithStatusJSON(401, gin.H{"error": "无证!"}) //立即中止请求链路+返回HTTP状态码与{JSON响应}gin.H类型(map[string]any)
return
}
// 2. 把 Bearer 去掉,只要后面的 token
parts := strings.SplitN(authHeader, " ", 2) //HTTP有三个要查看的,去除一个还有两个,返回的字符串数组
if len(parts) != 2 || parts[0] != "Bearer" {
c.AbortWithStatusJSON(401, gin.H{"error": "证件格式不对!"}) //立即中止请求链路+返回HTTP状态码与{JSON响应}gin.H类型(map[string]any)
return
}
// 3. 验真伪
username, err := utils.ParseToken(parts[1]) //第三个为密钥,用utils包中的验证来查看
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "证件过期或造假!"})
return
}
// 4. 重点!把解析出来的用户名,塞进上下文里,传给里面的经理
c.Set("username", username) //存储键值对数据
//二者实现了Gin的链路操作
c.Next() // 放行,将数据给后续的
}
}
// 1. 外交官:允许跨域
// 浏览器出于安全,默认禁止 JS 跨域名请求,这里是告诉浏览器“我允许”,此处我表示看不懂。。。
/*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*") // 允许任何人访问
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")
c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS") // 允许的方法
// OPTIONS 是浏览器的“预检”请求,问服务器支不支持。
// 如果是 OPTIONS,直接告诉它支持(204 No Content),不需要再往下处理了。
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
/*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/
package dao//可以看出dao包用于数据库的处理
import (
"encoding/json"
"os"
"painting/model"
"sync"
)
var paintMu sync.Mutex
var UserMap = map[string]string{}
var WorksMap = map[string][]model.Work{}
var dbFile = "database.json"
type DataStorage struct {
Users map[string]string `json:"users"`
Works map[string][]model.Work `json:"works"`
}
func Init() { //初始化
file, err := os.ReadFile(dbFile) //打开数据库dbFile放入file中
if err == nil { //无错
var data DataStorage //声明临时数据库结构
json.Unmarshal(file, &data) //将file(JSON)转换到data(临时数据库)
UserMap = data.Users //将临时数据库的数据存入传输数据中
WorksMap = data.Works //同上
}
// 防止空指针
if UserMap == nil { //即数据库中无内容则初始化
UserMap = make(map[string]string)
}
if WorksMap == nil {
WorksMap = make(map[string][]model.Work)
}
}
// 向数据库中写入数据
func Save() {
paintMu.Lock() //将要对数据库操作
defer paintMu.Unlock() //最后解锁
data := DataStorage{ //将传输数据转到临时库结构中
Users: UserMap,
Works: WorksMap,
}
file, _ := json.MarshalIndent(data, "", " ") //将临时库的数据转换成JSON进入file中
os.WriteFile(dbFile, file, 0666) //将file(Json内容)存入数据库中
}
// 实现添加作品逻辑
func AddWork(username string, work model.Work) { //参数为要保存的数据,参考model中,从此处可以看出model用于定义数据结构
work.Author = username //此处为本项目特殊要求,因为work数据结构中需要作者名
WorksMap[username] = append(WorksMap[username], work) //将work放入传输数据中
Save() //将传输数据保存
}
// 实现获取work逻辑,应该也为项目特殊
func GetWorks(username string) []model.Work {
return WorksMap[username]
}
// 加人(给注册用)
func AddUser(username, password string) bool {
if _, ok := UserMap[username]; ok {
return false // 已存在,即不为空
}
UserMap[username] = password
Save() //保存
return true
}
// 查人(给登录用)
func CheckUser(username, password string) bool { //通过从context读入username
// ok 代表 map 里有没有这个 key(人存不存在)
// pwd 是 map 里存的真密码
if pwd, ok := UserMap[username]; ok && pwd == password {
return true
}
return false
}
package api
import (
"painting/dao"
"painting/model"
"painting/utils"
"github.com/gin-gonic/gin"
)
// 注册
func Register(c *gin.Context) {
// ShouldBindJSON 自动把前端传来的 JSON 对照着 model.User 的结构填进去
// 如果格式不对(比如传了 int 而不是 string),err 就不为空
var u model.User //临时创建一个结构(model中的User)
if err := c.ShouldBindJSON(&u); err != nil { //将context的`json:"username"`与`json:"password"`放入u中
c.JSON(400, gin.H{"error": "参数填错了"})
return
}
if dao.AddUser(u.Username, u.Password) {
c.JSON(200, gin.H{"message": "注册成功!"})
} else {
c.JSON(400, gin.H{"error": "用户名已存在"})
}
}
// 登录
func Login(c *gin.Context) {
var u model.User
if err := c.ShouldBindJSON(&u); err != nil {
c.JSON(400, gin.H{"error": "参数填错了"})
return
}
if dao.CheckUser(u.Username, u.Password) {
token, _ := utils.GenerateToken(u.Username)
c.JSON(200, gin.H{
"message": "登录成功",
"token": token, // 把证件发给用户
})
} else {
c.JSON(401, gin.H{"error": "用户名或密码错误"})
}
}
// 挂画(需要登录)
func Upload(c *gin.Context) {
// 从保安那儿知道是谁
// 从上下文里拿用户名
// 这个 "username" 是 AuthMiddleware 里用 c.Set 塞进去的
username, _ := c.Get("username")
var w model.Work
if err := c.ShouldBindJSON(&w); /*向临时w写入JSON中的model.Work成功返回nil*/ err != nil {
c.JSON(400, gin.H{"error": "画的信息没填对"})
return
}
dao.AddWork(username.(string), w)
c.JSON(200, gin.H{"message": "上传成功", "work": w})
}
// 看展(公开)
func View(c *gin.Context) {
who := c.Param("who")
works := dao.GetWorks(who)
c.JSON(200, gin.H{"owner": who, "works": works})
}
package main
import (
"painting/api"
"painting/dao"
"painting/middleware"
"github.com/gin-gonic/gin"
)
func main() {
// 1. 仓库管理员先上班,准备好账本
dao.Init()
// 2. 启动 Gin 引擎(默认带了 Logger 和 Recovery 中间件,防崩坏)
r := gin.Default()
// 3. 全局中间件:安排外交官(CORS)站在大门口,所有请求都要经过它
r.Use(middleware.Cors())
//公共区(不需要证件)
r.POST("/register", api.Register) //注册
r.POST("/login", api.Login) //登录
r.GET("/gallery/:who", api.View) //看展
// :who 是动态参数,匹配如 /gallery/jack
// VIP区(必须有证件【密钥】)
authGroup := r.Group("/my")
/*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/
authGroup.Use(middleware.AuthMiddleware()) // 安排保安
{
authGroup.POST("/upload", api.Upload)
}
/*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/
// 4. 开业!默认监听 0.0.0.0:8080
/*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/
r.Run(":8080")
/*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/
}