Go入门: gin 框架极速搭建图书管理系统


前言

本项目适合 Golang 初学者,通过简单的项目实践来加深对 Golang 的基本语法和 Web 开发的理解。

项目结构

D:.
├─ go.mod
├─ go.sum
│  
├─ cmd
│  └─ main
│      └─ main.go
│
└─ pkg
    ├─ config
    │      └─ app.go
    │
    ├─ controllers
    │      └─ book-controller.go
    │
    ├─ models
    │      └─ book.go
    │
    └─ routes
       └─ bookstore-routes.go

项目流程图

  1. 技术栈

技术栈

  1. 项目结构

项目结构图

  1. 项目路由

项目路由图

项目初始化

  1. 初始化项目文件夹
md go-bookstore
  1. 初始化 mod 文件
cd go-bookstore
go mod init github.com/your_username/go-bookstore

注意,此处的 your_username 请替换为你的 GitHub 用户名

  1. 安装依赖包
# GORM 是一个用于 Go 的 ORM(对象关系映射)库,可以简化与数据库的交互。
go get "github.com/jinzhu/gorm"
# 这个包提供了与 MySQL 数据库的连接和操作功能,使得可以使用 GORM 进行 MySQL 数据库的 CRUD 操作。
go get "github.com/jinzhu/gorm/dialects/mysql"
# gin 是一个 Go 语言的 Web 框架,它可以快速搭建 Web 服务。
go get "github.com/go-gonic/gin"

# 打开 VSCode 编辑器
code .
  1. 创建 go-bookstore 数据库

打开 Mysql 客户端,输入以下命令创建 go-bookstore 数据库:

CREATE DATABASE `database_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

其中,database_name 请替换为你自己喜欢的数据库名称。

  1. 初始化项目结构

一行代码在项目根目录下创建目录和空文件

# windows系统
mkdir cmd\main pkg\config pkg\controllers pkg\models pkg\routes & type nul > cmd\main\main.go & type nul > pkg\config\app.go & type nul > pkg\controllers\book-controller.go & type nul > pkg\models\book.go & type nul > pkg\routes\bookstore-routes.go

# Linux/mac系统
mkdir -p cmd/main pkg/config pkg/controllers pkg/models pkg/routes && touch touch cmd/main/main.go pkg/config/app.go pkg/controllers/book-controller.go pkg/models/book.go pkg/routes/bookstore-routes.go

项目编写

  1. 编写 pkg/routes/bookstore-routes.go 文件

为什么要最先编写路由?

优先选择编写路由文件的原因在于路由决定了用户访问的 URL 所对应的页面和内容。也就是说,路由是用户请求的起点。

因为所有操作都从请求接口开始,定义好路由可以帮助我们明确应用的整体结构。

在路由确定之后,我们可以进一步编写控制器和模型,这样可以确保应用的各个部分都能协调工作。

虽然每个人的开发习惯和业务逻辑可能不同,但从路由入手通常是一个推荐的方法,它能帮助你更清晰地组织代码, 并且让你曾经觉得难以完成的独立开发一个项目变得轻松可行。

package routes

import (
    // 引入控制器
    "github.com/your_username/go-bookstore/pkg/controllers"
    // 引入 Gin 框架
	  "github.com/gin-gonic/gin"
)

// 定义路由组及其路由
func Router() *gin.Engine {
    r := gin.Default()

    book := r.Group("/book")
    {
        book.GET("/", controllers.GetBookTest)
        book.GET("/:bookId", controllers.GetBookByIdTest)
        book.POST("/", controllers.CreateBookTest)
        book.PUT("/:bookId", controllers.UpdateBookTest)
        book.DELETE("/:bookId", controllers.DeleteBookTest)
    }
    return r
}

注意,此处的 your_username 请替换为你的 GitHub 用户名。

  1. 引入控制器和 Gin 框架:引入了自定义的控制器包 controllersGin 框架的包 github.com/gin-gonic/gin
  2. gin.Default():使用 gin.Default() 创建一个默认的 Gin 引擎实例,该实例已包含了默认的中间件,如日志记录和恢复中间件。
  3. 定义路由组r.Group("/book") 创建了一个路由组,所有以 /book 开头的路径都将由这个组处理。
  4. 路由配置
    • GET /:映射到 controllers.GetBookTest,用于获取所有书籍的测试数据。
    • GET /:bookId:映射到 controllers.GetBookByIdTest,根据书籍 ID 获取指定书籍的信息。
    • POST /:映射到 controllers.CreateBookTest,用于创建新书籍。
    • PUT /:bookId:映射到 controllers.UpdateBookTest,根据书籍 ID 更新书籍信息。
    • DELETE /:bookId:映射到 controllers.DeleteBookTest,根据书籍 ID 删除书籍。
  5. 返回路由:在函数 Router() 的最后,返回了创建的 *gin.Engine 实例 r; 其中 *gin.Engine 实例是整个应用的核心路由处理对象,包含了所有定义的路由和中间件; 返回这个实例是为了在应用的其他部分(例如 main 函数)可以使用它来启动 HTTP 服务器或进行其他操作。
  1. 编写 pkg/config/app.go 文件
package config

import (
    "fmt"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

var (
    db *gorm.DB
)

func Connect() {
    // 其中root对应你的数据库用户名,yourpassword对应你的数据库密码,database_name对应你的数据库名称。
    d, err := gorm.Open("mysql", "root:your_password@tcp(127.0.0.1:3306)/go-bookstore03?charset=utf8mb4&parseTime=True&loc=Local")
    if err != nil {
      panic(err)
    }
    fmt.Println("Connect to database successfully")
    db = d
  }

func GetDB() *gorm.DB {
    return db
}

注意,此处的 your_password 请替换为你的数据库密码。

  1. 引入包:使用 gorm 和 MySQL 驱动。
  2. 全局变量db 存储数据库连接实例。
  3. Connect 函数:连接到 MySQL 数据库,连接成功后保存连接实例到 db。如果连接失败,程序会终止。
  4. GetDB 函数:返回 db,让其他代码可以使用数据库连接。
  5. 连接字符串参数charset=utf8mb4:支持更多字符。 parseTime=True:正确解析时间字段。 loc=Local:使用本地时区。
  1. 编写 pkg/models/models.go 文件
package models

import (
    "github.com/jinzhu/gorm"
    "github.com/Done-0/go-bookstore/pkg/config"
)

var db *gorm.DB

type Book struct {
    // 嵌套 gorm.Model 结构体,它包含了默认的字段:ID(主键)、CreatedAt、UpdatedAt 和 DeletedAt(软删除)。
    gorm.Model
    Name string `json:"name"`
    Author string `json:"author"`
    Publication string `json:"publication"`
}

// init 函数是 Go 语言中的特殊函数,它会在包初始化时自动执行。通常用来做一些初始化操作,如设置数据库连接、初始化配置等。
func init() {
    config.Connect()
    db = config.GetDB()
    // 使用 GORM 的 AutoMigrate 方法自动迁移 Book 结构体。
    // 自动迁移会创建或更新数据库表,使其与 Book 结构体匹配。如果表不存在,则创建表;如果表已存在,则更新表结构以匹配 Book 结构体的定义。
    db.AutoMigrate(&Book{})
}

// CreateBook 方法用于创建 Book 结构体的实例并插入到数据库中。
func (b *Book) CreateBook() *Book{
    db.NewRecord(b)
    db.Create(&b)
    return b
}

// GetAllBooks 方法用于从数据库中获取所有 Book 结构体的实例。
func GetAllBooks() []Book {
    var Books []Book
    db.Find(&Books)
    return Books
}

// GetBookById 方法用于从数据库中获取指定 ID 的 Book 结构体的实例。
func GetBookById(Id int64) (*Book, *gorm.DB) {
    var getBook Book
    db:=db.Where("ID=?", Id).Find(&getBook)
    return &getBook, db
}

// DeleteBook 方法用于从数据库中删除指定 ID 的 Book 结构体的实例。
func DeleteBook(ID int64) Book {
    var book Book
    // 软删除,将 DeletedAt 字段设置为当前时间。
    db.Where("ID=?", ID).Delete(&book)
    return book
}
  1. 依赖包:使用 gormconfig 包。
  2. 全局数据库变量db 用于存储数据库连接实例。
  3. Book 结构体:定义了书籍模型,包括 IDCreatedAtUpdatedAtDeletedAt(GORM 提供的默认字段)以及 NameAuthorPublication
  4. init 函数:连接数据库,并自动迁移 Book 表。
  5. CreateBook 方法:创建并插入新的书籍记录。
  6. GetAllBooks 方法:获取所有书籍记录。
  7. GetBookById 方法:根据 ID 获取特定书籍记录。
  8. DeleteBook 方法:根据 ID 删除特定书籍记录。
  1. 编写 cmd/main/main.go 文件
package main

import (
    "github.com/your_name/go-bookstore/pkg/routes"
)

func main() {
    r := routes.Router()

    r.Run(":9010")
}

注意,此处的 your_name 请替换为你的 GitHub 用户名。
可以注意到,引入 gin 框架 -> 调用 Router() 函 -> 启动 HTTP 服务,这一条逻辑线明显比使用纯原生的 net/http 包更加简洁。

  1. 编写 pkg/controllers/book-controller.go 文件
package controllers

import (
    "fmt"
    "net/http"
    "strconv"
    // 请替换 your_name 为你的 GitHub 用户名
    "github.com/your_name/go-bookstore/pkg/models"
    "github.com/gin-gonic/gin"
)

// NewBook 是一个用来创建新书的结构体
var NewBook models.Book

// GetBookTest 函数返回所有的书籍
func GetBookTest(c *gin.Context) {
    newBooks := models.GetAllBooks()
        c.JSON(http.StatusOK, newBooks)
}

// GetBookByIdTest 函数返回指定ID的书籍
func GetBookByIdTest(c *gin.Context) {
    bookId := c.Param("bookId")
    ID, _ := strconv.ParseInt(bookId,0,0)
    bookDetails, _ := models.GetBookById(ID)
      // 以JSON格式返回书籍详情
    c.JSON(http.StatusOK, bookDetails)
}

// CreateBookTest 函数创建一个新的书籍并返回详细信息
func CreateBookTest(c *gin.Context) {
    // 初始化一个新书籍结构体
    var CreateBook = &models.Book{}
    // 从请求体中解析书籍信息
    c.ShouldBindJSON(CreateBook)
    // 创建书籍并保存到数据库
    b := CreateBook.CreateBook()
    // 以JSON格式返回书籍详情
    c.JSON(http.StatusOK, b)
}

func DeleteBookTest(c *gin.Context) {
    bookId := c.Param("bookId")
    ID, err := strconv.ParseInt(bookId,0,0)
    if err != nil {
      fmt.Printf("解析错误!")
    }
    // 删除之前获取书籍详情, 并删除书籍
    book, _ := models.GetBookById(ID)
    models.DeleteBook(ID)
    c.JSON(http.StatusOK, book)
}

func UpdateBookTest(c *gin.Context) {
    var updateBook = &models.Book{}
    c.ShouldBindJSON(updateBook)
    bookId := c.Param("bookId")
    ID, err := strconv.ParseInt(bookId,0,0)
    if err != nil {
        fmt.Println("解析错误!")
    }
    bookDetails, db := models.GetBookById(ID)
    // 如果Name、Author、Publication字段有更新,则更新数据库对应数据
    if updateBook.Name != "" {
        bookDetails.Name = updateBook.Name
    }
    if updateBook.Author != "" {
        bookDetails.Author = updateBook.Author
    }
    if updateBook.Publication != "" {
        bookDetails.Publication = updateBook.Publication
    }
    // 保存更新后的书籍信息到数据库
    db.Save(&bookDetails)
    c.JSON(http.StatusOK, bookDetails)
}

注意,此处的 your_name 请替换为你的 GitHub 用户名。

为什么在开头创建一个空的结构体变量?

1. 清晰的数据管理

结构体用途

  • models.Book:用来表示书籍的完整数据模型,包括所有字段,如NameAuthorPublication等。
  • NewBook:作为全局变量,实际用处不大,可能是用于创建新的书籍实例,但它的实际用法可能会产>生混淆。

2. 灵活的请求处理

具体代码应用

  1. CreateBookTest 函数

    • 目的:处理创建新书籍的请求。
    • 结构体用法:创建了一个新的models.Book实例来接收客户端提交的书籍数据。
    • 为什么这样做:通过解析请求体中的 JSON 数据填充这个结构体,然后用它创建新书籍。这确保了处理请求时,数据结构与数据库模型一致。
    func CreateBookTest(c *gin.Context) {
        var CreateBook = &models.Book{}
        c.ShouldBindJSON(CreateBook)
        b := CreateBook.CreateBook()
        c.JSON(http.StatusOK, b)
    }
    
  2. UpdateBookTest 函数

    • 目的:处理更新书籍信息的请求。
    • 结构体用法:创建一个models.Book实例来接收更新的数据。
    • 为什么这样做:结构体的字段用于确定哪些书籍属性需要更新。这使得更新逻辑更加清晰,并且只更新客户端提供的字段。
    func UpdateBookTest(c *gin.Context) {
        var updateBook = &models.Book{}
        c.ShouldBindJSON(updateBook)
        bookId := c.Param("bookId")
        ID, err := strconv.ParseInt(bookId,0,0)
        if err != nil {
            fmt.Println("解析错误!")
        }
        bookDetails, db := models.GetBookById(ID)
        if updateBook.Name != "" {
            bookDetails.Name = updateBook.Name
        }
        if updateBook.Author != "" {
            bookDetails.Author = updateBook.Author
        }
        if updateBook.Publication != "" {
            bookDetails.Publication = updateBook.Publication
        }
        db.Save(&bookDetails)
        c.JSON(http.StatusOK, bookDetails)
    }
    

3. 数据验证和清理

在处理创建和更新请求时,使用不同的结构体可以方便地进行数据验证。例如,可以在 CreateBookTest 中要求所有字段都必须提供,而在 UpdateBookTest 中只更新那些提供了新值的字段。这种做法使得每个请求的处理更加灵活。

4. 代码可维护性

结构体的清晰定义

  • models.Book:定义了书籍的完整数据结构。
  • CreateBookupdateBook:专门用于请求处理,便于在实际操作中进行数据绑定和验证。

这种做法使得代码的每个部分都专注于自己需要处理的任务,从而提高了代码的可读性和可维护性。即使在未来需要对请求格式或数据模型进行修改,也能在对应的位置进行调整,而不影响其他部分的代码。

运行项目

  1. 编译项目
go run cmd/main/main.go
  1. postman 测试

进入Postman测试接口,验证项目功能是否正常:
GET ALL 接口地址:http://localhost:9010/book/
GET BY ID 接口地址:http://localhost:9010/book/1
CREATE 接口地址:http://localhost:9010/book/
UPDATE 接口地址:http://localhost:9010/book/1
DELETE 接口地址:http://localhost:9010/book/1

书籍创建之 JSON 模板:

{
    "ID": 1,
    "createdAt": "2024-08-29T13:26:00Z",
    "updatedAt": "2024-08-29T13:26:00Z",
    "DeleteAt": null,
    "name": "Go 语言",
    "author": "老子",
    "publication": "人民邮电出版社",
}

postman测试