Go Fiber:开始在 Golang 上构建 RESTful API(Feat. GORM)
📚 内容
让我们用简单的英语学习科技
如果您更擅长编写代码而不是文本,请直接跳转到已完成的项目,对其进行分叉并开始添加您的代码 -
percoguru /教程笔记-api-go-fiber
使用 Go 构建 RESTful API:功能包括 Fiber、Postgres、GORM
👷 我们正在构建什么 ↑
我们将用 Go 语言构建一个简单的笔记 API,以建立一个基础代码库,用于扩展和创建完整的后端服务。我们创建的框架将帮助您理解并开始使用 Go 语言的 API。
如果您想知道如何开始用 Go 语言开发 API,或者您刚刚掌握 Go 语言的基础知识并希望开始实际开发,那么这将是一个绝佳的起点。
🚅 前往 ↑
Go 语言已经存在十多年了。它深受人们喜爱,学习和使用起来也非常有趣。
虽然可以使用 Go 构建功能齐全的全栈 Web 应用程序,但本文旨在学习如何用 Go 语言构建基本的 Web API,因此我们将只讨论 API。
⏩ Express 和 Fiber ↑
过去十年来,Node.js 一直深受后端服务开发者的喜爱。
使用 Express 方式开发 API,无论对经验丰富的 Node.js 开发者还是 Go 语言新手来说都非常友好。因此,Fiber 的构建也充分考虑了 Express 的理念。Fiber
是一个 Go 语言的 Web 框架,它从零开始就为 API 做好了准备,支持中间件,并且性能超快。我们将使用 Fiber 来创建我们的 Web API。
📁 最终项目结构
让我们首先看看我们的最终结构是什么样的 -
notes-api-fiber
|
- config
| - config.go
|
- database
| - connect.go
| - database.go
|
- internal
| |
| - handler
| | - note
| | - noteHandler.go
| |
| - model
| | - model.go
| |
| - routes
| - note
| - noteRoutes.go
|
- router
| - router.go
|
- main.go
- go.mod
- go.sum
不用担心,我们将从一个文件开始构建,并在构建过程中通过足够的逻辑和解释达到这一状态。
🎒 软件包基础知识 -
Go 代码以包的形式分发,我们将会创建很多这样的包。Go 包用于根据用途分发代码和逻辑。这在我们上面绘制的目录结构中也可以看到。
package <package_name>
我们通过在顶部写入来声明文件的包名称。- 简单来说,包是 go 项目中共享变量和函数的一组代码。
- 同一包中的任意数量的文件共享变量、函数、类型、接口,基本上任何定义。
- 为了正确访问包的任何代码,包的所有文件都应位于单个目录中。
💡 让我们开始使用 API ↑
我们将从一个文件开始,这是我们代码的起点 - main.go
。在我们项目的根目录中创建此文件。
主程序 ↑
我们先从编写根文件开始main.go
。这将是应用程序的起点。现在,我们只在这里初始化一个 Fiber 应用。稍后我们会添加更多内容,这里将成为设置发生的地方。
主程序
package main
import (
"github.com/gofiber/fiber/v2"
)
func main() {
// Start a new fiber app
app := fiber.New()
// Listen on PORT 300
app.Listen(":3000")
}
让我们创建一个端点 ↑
为了对如何创建 API 端点有一个基本的了解,我们首先创建一个虚拟端点来开始。
如果你曾经使用过 Express,你可能会注意到它们之间的相似之处;如果你没有使用过 Express,你就会发现它们之间的相似之处恰恰相反。
主程序
package main
import (
"github.com/gofiber/fiber/v2"
)
func main() {
// Start a new fiber app
app := fiber.New()
// Send a string back for GET calls to the endpoint "/"
app.Get("/", func(c *fiber.Ctx) error {
err := c.SendString("And the API is UP!")
return err
})
// Listen on PORT 3000
app.Listen(":3000")
}
通过运行运行服务器
go run main.go
在根目录中。然后转到localhost:3000
。您将看到如下页面:
现在,我们已经了解了如何从包含几行代码的单个文件启动 API。
请注意,您可以继续在此处添加更多端点并进行扩展。这种情况在本文中会多次出现,但我们不会垂直扩展,而是尽可能水平分布代码。
现在我们的项目目录看起来像 -
notes-api-fiber
|
- main.go
💼 Go 模块 ↑
我们的项目将成为一个 Go 模块。要了解更多关于 Go 模块的信息,请访问https://golang.org/ref/mod。要在项目目录中启动 Go 模块,请运行以下命令:
go mod init <your_module_url>
通常<your_module_url>
表示您的模块将发布到哪里。目前,您可以使用您的 Github 个人资料。例如 - 如果您的 Github 用户名是percoguru
,您将运行
go mod init github.com/percoguru/notes-api-fiber
路径的最后一部分是项目名称。
现在,您创建的任何包都将成为此模块内的子目录。例如,一个包foo
将被模块中的另一个包以 的形式导入github.com/percoguru/notes-api-fiber/foo
。
运行该命令后,go.mod
将创建一个文件,其中包含有关我们模块的基本信息以及我们将要添加的依赖项。这可以被视为package.json
Go 的等效文件。
你的go.mod
遗嘱如下:
go.mod
module github.com/percoguru/notes-api-fiber
go 1.17
📐 设置要点 ↑
现在我们将设置一些基本的东西来支持我们的 API -
- Makefile
- 数据库(PostgreSQL)
- 模型
- 环境变量
Makefile ↑
随着我们引入越来越多的更改,go run main.go
每次我们希望更改反映在正在运行的服务器上时,都需要运行。要启用服务器的热重载,请Makefile
在目录的根目录中创建一个。
- 安装反射
go install github.com/cespare/reflex@latest
- 将命令添加到您的
Makefile
build:
go build -o server main.go
run: build
./server
watch:
reflex -s -r '\.go$$' make run
- 跑步
make watch
- 对您的代码进行任何更改并在终端中查看服务器重新加载。
💾 数据库 ↑
在本文中,我们将使用 Postgres 作为数据库实现。
虽然我们这里只是开发一个笔记应用,但本文的目的是帮助你在此基础上进行扩展。我甚至鼓励你现在就尝试一下数据库模式,添加新的实体或使用除笔记之外的其他东西。SQL 非常适合这种扩展,如果你决定在此基础上进一步扩展,就无需再探索其他选项了。
在您的机器上运行 Postgres,并为我们的实现创建一个数据库 fiber-app - 按照https://www.postgresql.org/download/上的说明进行操作
⚙️ 添加配置 ↑
我们将.env
在项目目录的根目录中添加一个环境变量文件。当我们连接到数据库时,我们需要一些变量,我们将这些变量存储在这个文件中。
.env
DB_HOST= localhost
DB_NAME= fiber-app
DB_USER= postgres
DB_PASSWORD= postgres
DB_PORT= 5432
除了密码之外,上述值很可能也保持不变,密码将由您为 postgres 用户选择。请记住在继续操作之前创建一个数据库 fiber-app。
现在我们需要从文件中获取这些变量.env
。为此,我们将创建一个文件,package config
用于提供项目的配置。在目录的根目录下
创建一个文件夹,并在其中创建一个文件。 首次运行 -config
config.go
go get github.com/joho/godotenv
config.go
package config
import (
"fmt"
"os"
"github.com/joho/godotenv"
)
// Config func to get env value
func Config(key string) string {
// load .env file
err := godotenv.Load(".env")
if err != nil {
fmt.Print("Error loading .env file")
}
// Return the value of the variable
return os.Getenv(key)
}
🔄 连接数据库 ↑
在项目的根文件夹中,创建一个名为 的目录database
。所有与数据库连接和迁移相关的代码都将驻留在此目录下。这将成为与数据库连接操作相关的包,我们将其命名为database
。
我们将使用 ORM(对象关系映射)作为 Go 代码和 SQL 数据库之间的中间件。本文将选择 GORM 作为 ORM 的首选。它支持 Postgres、关联、钩子以及一项对我们初期帮助很大的功能——自动迁移。
运行以下命令添加 gorm 及其 postgres 驱动程序:
go get gorm.io/gorm
go get gorm.io/driver/postgres
连接.go
package database
import (
"fmt"
"log"
"strconv"
"github.com/percoguru/notes-api-fiber/config"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
// Declare the variable for the database
var DB *gorm.DB
// ConnectDB connect to db
func ConnectDB() {
var err error
p := config.Config("DB_PORT")
port, err := strconv.ParseUint(p, 10, 32)
if err != nil {
log.Println("Idiot")
}
// Connection URL to connect to Postgres Database
dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", config.Config("DB_HOST"), port, config.Config("DB_USER"), config.Config("DB_PASSWORD"), config.Config("DB_NAME"))
// Connect to the DB and initialize the DB variable
DB, err = gorm.Open(postgres.Open(dsn))
if err != nil {
panic("failed to connect database")
}
fmt.Println("Connection Opened to Database")
}
注意如何connect.go
导入包config
。它在文件夹内查找包./config
。
我们已经创建了数据库连接器,但运行应用程序时会运行go run main.go
。目前我们还没有调用函数connectDB()
。我们需要调用它才能连接到数据库。
让我们转到main.go
文件并连接到数据库。我们希望在服务器运行时连接到数据库。因此,我们可以从包 main 的connectDB()
函数中调用该函数。main
主程序
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/percoguru/notes-api-fiber/database"
)
func main() {
// Start a new fiber app
app := fiber.New()
// Connect to the Database
database.ConnectDB()
// Send a string back for GET calls to the endpoint "/"
app.Get("/", func(c *fiber.Ctx) error {
err := c.SendString("And the API is UP!")
return err
})
// Listen on PORT 3000
app.Listen(":3000")
}
现在运行
go run main.go
我们尚未对数据库进行任何操作。我们将创建模型来表示我们想要存储在数据库中的表。
🔖 添加模型
internal
在目录根目录下创建一个文件夹。我们所有的内部逻辑(模型、类型、处理程序、路由、常量等)都将存储在此文件夹中。
在此文件夹中创建一个文件夹model
,然后创建一个文件model.go
。这样就创建了internal/model/model.go
。文件夹 model 将包含我们的package model
。
我们目前只创建一个模型 - note,您可以在此处添加更多模型,或者使用 note 以外的其他模型,例如product、page等。一定要发挥创造力。即使您选择使用其他模型(顺便说一句,我鼓励您这样做😉),代码的其他部分也基本保持不变,所以,请大胆尝试。
我们的 Notes 表如下所示 -
- ID uuid
- 标题文本
- 副标题文本
- 文本文本
要在 Go 中使用 uuid 类型,请运行 -
go get github.com/google/uuid
要创建此模型,请将其添加到model.go
文件中 -
package model
import (
"github.com/google/uuid"
"gorm.io/gorm"
)
type Note struct {
gorm.Model // Adds some metadata fields to the table
ID uuid.UUID `gorm:"type:uuid"` // Explicitly specify the type to be uuid
Title string
SubTitle string
Text string
}
请注意
ID uuid.UUID `gorm:"type:uuid"`
我们首先告诉 Go 这个结构字段的类型,uuid.UUID
然后通过指定标签告诉 GORM 创建具有 uuid 类型的列gorm:"type:uuid"
🔁 自动迁移
GORM 支持自动迁移,因此无论何时您对模型结构进行更改(例如添加列、更改类型、添加索引)并重新启动服务器,更改都会自动反映在数据库中。
请注意,为了避免意外丢失数据,如果您在结构体中执行了迁移操作,GORM 不会自动删除列。不过,您可以配置 GORM 来执行此操作。
进入你的database/connect.go
文件并添加一行以在连接到数据库后自动迁移
...
// Connect to the DB and initialize the DB variable
DB, err = gorm.Open(postgres.Open(dsn))
if err != nil {
panic("failed to connect database")
}
fmt.Println("Connection Opened to Database")
// Migrate the database
DB.AutoMigrate(&model.Note{})
fmt.Println("Database Migrated")
}
现在,重启服务器。您的数据库已迁移。您可以登录 Postgres 来验证迁移是否成功;在 Linux 上,您可以使用 psql;在 Windows 上,您可以使用 pgAdmin 或 DBeaver。
在 Ubuntu 上,使用 psql时:
- 连接到数据库 notes-api
\c notes-api
- 获取有关注释表的详细信息请注意,GORM 将结构名称复数化为表名称,另请注意下图中如何根据结构中的相应字段名称处理字段名称
\d+ notes
输出:当我们在结构体顶部 添加时,GORM 已添加额外的字段。 现在,我们已经验证了迁移,并且模型已保存在数据库中。😎gorm.Model
现在让我们重新审视一下我们的项目结构 -
notes-api-fiber
|
- config
| - config.go
|
- database
| - connect.go
|
- internal
| |
| |- model
| | - model.go
|
- main.go
- go.mod
- go.sum
- .env
🚡 添加路线 ↑
我们的 API 将包含路由,这些路由是浏览器、Web 或移动应用程序用来对数据执行 CRUD 操作的端点。
首先,我们设置一个基本的路由器来启动 Fiber 应用的路由。在项目的根目录中
创建一个文件夹router
,并在其中创建一个文件router.go
。
在文件中添加以下代码:
package router
import "github.com/gofiber/fiber/v2"
func SetupRoutes(app *fiber.App) {
}
我们刚刚在 router 包中声明了一个函数 SetupRoutes,它接受一个 Fiber 应用作为参数。该函数将接收一个 Fiber 应用,并将对该应用的调用路由到特定的路由或路由处理程序。API
根据参数分组,Fiber 允许我们这样做。例如,如果有三个 API 端点:
- GET
api/user/:userId
-通过 userId 获取用户 - GET——
api/user
获取所有用户 - PUT
api/user/:userId
-使用 userId 更新用户
我们不必写出所有重复的参数。我们可以这样做:
...
app := fiber.App()
api := app.Group("api")
user := api.Group("user")
user.GET("/", func(c *fiber.Ctx) {} )
user.GET("/:userId", func(c *fiber.Ctx) {} )
user.PUT("/:userId" ,func(c *fiber.Ctx) {} )
当我们扩展并添加大量 API 和复杂路由时,这非常有用。
请注意,“api”的类型为 fiber.Router,“app”的类型为 fiber.App,并且它们都具有 Group 函数。
🔨路线 ↑
我们希望保留像这样的路线SERVER_HOST/api/param1/param2
。因此,我们将这行添加到我们的函数中SetupRoutes
-
api := app.Group("/api", logger.New()) // Group endpoints with param 'api' and log whenever this endpoint is hit.
处理程序logger.New()
还将记录所有 API 调用及其状态。
现在,正如我之前所说,我们将水平扩展,我们可以在main.go
文件本身中添加与注释相关的路由,但我们创建了一个路由器包来处理 API 路由。现在,当 API 扩展时,您将添加很多模型,因此我们无法在路由器包中添加添加注释 API。
我们将为每个模型添加特定的路由器,现在为 Notes
在文件夹 内internal
创建一个文件夹,routes
我们将在其中为与模型相关的所有路由创建子目录。在 routes 文件夹中添加一个文件夹,note
并在文件夹内添加一个文件note.go
。
您已创建 -
internal/routes/note/note.go
在文件中note.go
添加以下代码 -
package noteRoutes
import "github.com/gofiber/fiber/v2"
func SetupNoteRoutes(router fiber.Router) {
}
函数 SetupNoteRoutes 接受一个 fiber.Router 参数,并处理笔记模型的端点。因此添加以下代码:
note := router.Group("/note")
我们将在笔记路由中添加 CRUD(创建、读取、更新、删除)操作。因此 -
package noteRoutes
import "github.com/gofiber/fiber/v2"
func SetupNoteRoutes(router fiber.Router) {
note := router.Group("/note")
// Create a Note
note.Post("/", func(c *fiber.Ctx) error {})
// Read all Notes
note.Get("/", func(c *fiber.Ctx) error {})
// Read one Note
note.Get("/:noteId", func(c *fiber.Ctx) error {})
// Update one Note
note.Put("/:noteId", func(c *fiber.Ctx) error {})
// Delete one Note
note.Delete("/:noteId", func(c *fiber.Ctx) error {})
}
请注意,我们必须编写处理程序函数来执行我们为所有 API 端点注释的任务,
我们将在单独的包中编写这些处理程序
🔧 处理程序 ↑
处理程序是接收 Fiber 上下文 (fiber.Ctx) 并处理请求、发送响应或仅充当中间件并将权限传递给下一个处理程序的函数。
要了解有关处理程序和中间件的更多信息,请访问Fiber 文档
。在文件夹 中internal
添加一个文件夹,handlers
该文件夹将包含所有 API 处理程序,每个模型都有一个特定的子目录。因此,请note
在其中创建一个文件夹,并在其中handlers
添加一个文件。 您刚刚创建了 -note.go
note
internal/handlers/note/note.go
现在我们将在routes/note/note.go
文件中添加我们需要的处理程序handlers/note/note.go
。
处理程序/note/note.go
package noteHandler
- 添加阅读笔记处理程序 -
func GetNotes(c *fiber.Ctx) error {
db := database.DB
var notes []model.Note
// find all notes in the database
db.Find(¬es)
// If no note is present return an error
if len(notes) == 0 {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No notes present", "data": nil})
}
// Else return notes
return c.JSON(fiber.Map{"status": "success", "message": "Notes Found", "data": notes})
}
- 添加创建注释处理程序 -
func CreateNotes(c *fiber.Ctx) error {
db := database.DB
note := new(model.Note)
// Store the body in the note and return error if encountered
err := c.BodyParser(note)
if err != nil {
return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Review your input", "data": err})
}
// Add a uuid to the note
note.ID = uuid.New()
// Create the Note and return error if encountered
err = db.Create(¬e).Error
if err != nil {
return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Could not create note", "data": err})
}
// Return the created note
return c.JSON(fiber.Map{"status": "success", "message": "Created Note", "data": note})
}
- 添加获取注释处理程序
func GetNote(c *fiber.Ctx) error {
db := database.DB
var note model.Note
// Read the param noteId
id := c.Params("noteId")
// Find the note with the given Id
db.Find(¬e, "id = ?", id)
// If no such note present return an error
if note.ID == uuid.Nil {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No note present", "data": nil})
}
// Return the note with the Id
return c.JSON(fiber.Map{"status": "success", "message": "Notes Found", "data": note})
}
- 添加更新注释处理程序
func UpdateNote(c *fiber.Ctx) error {
type updateNote struct {
Title string `json:"title"`
SubTitle string `json:"sub_title"`
Text string `json:"Text"`
}
db := database.DB
var note model.Note
// Read the param noteId
id := c.Params("noteId")
// Find the note with the given Id
db.Find(¬e, "id = ?", id)
// If no such note present return an error
if note.ID == uuid.Nil {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No note present", "data": nil})
}
// Store the body containing the updated data and return error if encountered
var updateNoteData updateNote
err := c.BodyParser(&updateNoteData)
if err != nil {
return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Review your input", "data": err})
}
// Edit the note
note.Title = updateNoteData.Title
note.SubTitle = updateNoteData.SubTitle
note.Text = updateNoteData.Text
// Save the Changes
db.Save(¬e)
// Return the updated note
return c.JSON(fiber.Map{"status": "success", "message": "Notes Found", "data": note})
}
- 添加删除注释处理程序
func DeleteNote(c *fiber.Ctx) error {
db := database.DB
var note model.Note
// Read the param noteId
id := c.Params("noteId")
// Find the note with the given Id
db.Find(¬e, "id = ?", id)
// If no such note present return an error
if note.ID == uuid.Nil {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "No note present", "data": nil})
}
// Delete the note and return error if encountered
err := db.Delete(¬e, "id = ?", id).Error
if err != nil {
return c.Status(404).JSON(fiber.Map{"status": "error", "message": "Failed to delete note", "data": nil})
}
// Return success message
return c.JSON(fiber.Map{"status": "success", "message": "Deleted Note"})
}
📨 将处理程序连接到路由 ↑
在注释路由中添加处理程序,将文件更改routes/note/note.go
为 -
路线/注释/note.go
package noteRoutes
import (
"github.com/gofiber/fiber/v2"
noteHandler "github.com/percoguru/notes-api-fiber/internals/handlers/note"
)
func SetupNoteRoutes(router fiber.Router) {
note := router.Group("/note")
// Create a Note
note.Post("/", noteHandler.CreateNotes)
// Read all Notes
note.Get("/", noteHandler.GetNotes)
// // Read one Note
note.Get("/:noteId", noteHandler.GetNote)
// // Update one Note
note.Put("/:noteId", noteHandler.UpdateNote)
// // Delete one Note
note.Delete("/:noteId", noteHandler.DeleteNote)
}
注意,由于文件夹名称和包名称不匹配,noteHandler 是如何导入的,如果要避免这种情况,请将文件夹也命名为 noteHandler
📨 设置笔记路线 ↑
在文件中设置注释路线router/router.go
,将其编辑为 -
路由器/router.go
package router
import (
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/logger"
noteRoutes "github.com/percoguru/notes-api-fiber/internals/routes/note"
)
func SetupRoutes(app *fiber.App) {
api := app.Group("/api", logger.New())
// Setup the Node Routes
noteRoutes.SetupNoteRoutes(api)
}
📨设置路由器 ↑
到目前为止,我们已经创建了路由器并用它来设置笔记路由,现在我们需要在主函数中设置这个路由器。在主
函数中,main.go
删除我们之前创建的虚拟端点,并添加以下代码:
router.setupRoutes(app)
将您的转化main.go
为 -
主程序
package main
import (
"github.com/gofiber/fiber/v2"
"github.com/percoguru/notes-api-fiber/database"
"github.com/percoguru/notes-api-fiber/router"
)
func main() {
// Start a new fiber app
app := fiber.New()
// Connect to the Database
database.ConnectDB()
// Setup the router
router.SetupRoutes(app)
// Listen on PORT 3000
app.Listen(":3000")
}
瞧!💥💰
我们做到了!
跑步
make watch
在 Postman 上尝试一下这些端点。一切运行良好。
接下来是什么?
现在,您已经用 Go 从零开始构建了一个 Web API。您了解了 Go、Fiber、GORM 和 Postgres 之间的细微差别。这是一个基本的设置,您可以将您的 API 扩展为一个全栈 Web 应用程序 -