GoClub Pro

Go-web实例

最近更新:2026-10-24
本页是手写/批注类笔记,直接按原始笔记截图呈现(转成文字会乱序);原始笔记文件见「Go语言相关笔记」页的原始链接。
onenote-go-web 原始笔记截图 1 onenote-go-web 原始笔记截图 2 onenote-go-web 原始笔记截图 3
转换文字版(供搜索 / 复制)

这份资源清单是专门为新手挑选的。我按照学习顺序和难易程度进行了分类,重点推荐了中文社区最优质的教程(特别是李文周的博客,被称为 Go 语言新手的“圣经”)。


1. HTTP 与网络基础 (必看理论)

在写代码前,你需要先搞懂“浏览器和服务器是怎么对话的”。

  • MDN HTTP 概述 (权威文档)
    • MDN Web Docs - HTTP
    • 推荐理由:Mozilla 的官方文档,全球公认最权威的前端/Web文档,内容非常详细。
  • 图解 HTTP (通俗易懂)


2. Go 语言原生 Web 开发 (net/http)

不要跳过这一步直接学 Gin,理解原生库才能看懂框架在做什么。


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)

如果你更喜欢看视频操作:

  1. 七米 (李文周):B站搜索“七米”,他的 Gin 框架视频教程和他的博客一样优秀。
  2. 极客兔兔 (Geektutu):搜索“7天用Go动手写/从零实现Web框架”,这个系列会带你从零造一个 Gin 框架,能让你彻底理解框架内部原理(适合 LvX 进阶)。

🚀 学习路线建议

  1. 先看 Go By Example 把 net/http 的 Hello World 跑通。
  2. 重点攻读 李文周的 Gin 教程,完成作业的 Lv0 - Lv2。
  3. 去 jwt.io 玩一下 Token,然后照着 李文周的 JWT 教程 完成 Lv3 - Lv4。
  4. 最后看 Go Web 编程 补充理论细节。
  5. 李文周的博客 | 总结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")

    /*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*/

}



内容反馈

发现错误或想补充内容?

GoClub Pro 是静态知识库,评论功能不再加载第三方脚本。你可以在 GitHub 仓库提交 Issue,反馈会直接进入项目协作流程。

提交 GitHub 反馈