如何使用Go Fiber和Gorm框架来运行Golang应用程序

1,085 阅读4分钟

如何使用Go Fiber和Gorm框架来运行Golang应用程序

Go是一种通用的语言。你可以用Go来构建Web应用、微服务、云服务、API、DevOps工具,以及任何应用。

Go Fiber是一个受Express启发的Golang框架。Go Fiber是一个建立在快速HTTP之上的网络框架。它可以用来处理路由/端点、中间件、服务器请求等操作。

在本教程中,我们将进一步了解Go Fiber。然后,我们将使用Go Fiber与Gorm和一个SQLite数据库来建立一个Todo应用程序。

前提条件

要继续学习本教程,请确保你有 Golang 的基本知识。

这包括

  • 在你的电脑上安装了Go,运行go version 命令来验证Go的安装。
  • 能够设置基本的Golang应用程序。
  • 运行和创建Golang应用程序,并了解如何编写Golang代码。

设置Go项目

像标准的基本应用程序一样,创建一个项目文件夹,用你喜欢的文本编辑器打开它。我将在本教程中使用Visual studio代码。

Go使用不同的库和框架。因此,在创建应用程序时,你需要在你的项目中保存这些包的二进制文件,以便你的应用程序能够访问它们。

Go也使用文件系统来保存模块的依赖性。这样就把Go模块与模块路径和与当前安装的模块相关的sematic版本保存起来。然后我们将这些模块导入到项目根目录下的应用程序中。

Go使用go.modgo.sum来管理这些依赖关系。go.mod 包含所有你安装的间接依赖关系和你想使用的版本。间接依赖不在项目内部使用,但被视为间接依赖。

另外,任何在go.mod 包中提到但在任何源文件中没有发现的依赖也被认为是间接依赖。go.sum 维护成功安装的包的校验。

如果你重新运行该项目,它不会安装所有的包。它通过将包的缓存存储在$GOPATH/pkg/mod 目录中来做到这一点。一个比较的例子是在使用Node.js和NPM时。在这种情况下,package.jsonpackage-lock.json 文件被用来管理 Node.js 的依赖性。

要初始化这些文件,在你的项目根目录下运行以下命令。

go mod init go-fiber-app

用下面的命令将创建一个go.mod

module go-fiber-app
go 1.17

在这种情况下,go-fiber-app 将是我们的直接模块和用于维护版本控制的模块声明。go-fiber-app 作为一个URL,用于在我们的应用程序中导入本地模块。go 1.17 是当前在你的计算机上运行的go版本。go.sum 将在之后我们开始安装我们的包时创建。

建立一个基本的Go Fiber服务器

让我们跳进去,用Go建立我们的第一个HTTP服务器,了解Go Fiber的最基本概念。就像Express一样,使用Fiber框架来启动你的第一个Go服务器是很简单的。

Go Fiber是一个受Express启发的框架。因此,让我们退一步,看看我们如何用Express创建一个简单的Node.js服务器。下面是一个利用Express框架的基本服务器。

// add Express library
const express = require("express");
// create a new Express instance
const app = express();
// Create a new endpoint
app.get("/", (req, res) => {
    // send text
    res.send("Hello World!");
});
// Start server on port 3000
app.listen(3000);

现在用Golang并使用Fiber框架,上面的Node.js例子工作起来也是一样的。只是一些语法发生了变化。同样的HTTP服务器被实现了,但是用不同的语言使用不同的框架。

让我们深入了解一下,看看我们如何用Go和Fiber创建一个外观相似的服务器。

首先,我们需要使用go get命令使Fiber对我们的应用程序可用。

让我们通过运行以下命令来安装它。

go get -u github.com/gofiber/fiber/v2

现在我们可以开始实现我们的第一个Go Fiber启发的HTTP服务器。继续前进,在你的项目文件夹中创建一个main.go 文件,然后按照以下步骤进行。

  • 添加主模块。

主模块包含在每个Go文件中。它将模块导入到Go本地文件中的其他模块。

package main
  • 由于我们使用的是Fiber,我们需要导入包来访问Go Fiber的函数和方法。
// add Fiber package
import "github.com/gofiber/fiber/v2" 
  • 添加一个main函数。

main函数是一个特殊的Go方法,定义了Golang应用程序的入口点。它被用来执行其他显式函数或Golang代码块。

func main() {
    // create a new Fiber instance
    app := fiber.New()

main 函数中,我们要创建一个新的Fiber实例。这将实例化一个Fiber应用程序。

  • 创建一个端点。

app.get 将设置我们的默认路由函数。它将接受一个 的Fiber上下文,并且它期待一个错误。这个 结构有各种很酷的东西。在我们的例子中,我们要发送一个纯文本字符串。context context

// Create a new endpoint
app.Get("/", func(c *fiber.Ctx) error {
    // send text
    return c.SendString("Hello, World!")
})
  • 定义服务器的端口。

然后设置一个端口号,我们的服务器将监听并在本地运行。

// Start server on port 3000
app.Listen(":3000")
}

该应用程序已经准备好了,我们现在可以通过运行以下命令来测试它。

go run main.go

输出。

go-fiber-simple-server

这就是你的成果。你最简单的Go Fiber HTTP服务器已经建立并运行。导航到http://127.0.0.1:3000/ ,浏览器上会显示一个简单的 "hello world "文本。

设置一个Fiber todos应用程序

以上是一个简单的HTTP服务器。现在让我们深入到一个更深刻的用例中,探索Go Fiber框架的更多内容。我们将使用一个带有SQLite数据库的todo应用程序用例来建立一个todos应用程序。

让我们从创建一个项目目录开始。然后,用go mod init go-fiber-todos 来初始化Go。

我们将安装以下软件包。

  • Gorm
go get -u gorm.io/gorm
  • Gorm SQLite驱动
go get -u gorm.io/driver/sqlite
go get github.com/google/uuid
  • 空气

你还记得Node.js服务器的Nodemon吗?Air的工作原理与Nodemon基本相同。

当你在建立一个服务器时,你可能需要看管你的文件。这样一来,你只需启动一次你的服务器。然后,当你对你的文件进行修改时,服务器又会自动重新启动。

Air在Go的开发包中。每当你修改你的代码时,它就会实时重新加载你的Go服务器。所以你可以把它设置好,然后专注于你的代码。

go get -u github.com/cosmtrek/air

为我们的项目设置Air

我们需要设置Air,以便应用程序能够处理实时重载。Air会看管项目文件和目录,构建,并通过给我们提供一些彩色的日志输出来运行应用程序。

air-live-reloads

为了初始化Air的运行。

air init

上述命令将创建一个.air.toml 文件到当前目录,并带有默认设置。从那里,你可以直接运行air ,开始看管你的开发服务器。或者运行air -d ,在调试环境下打印所有日志。

.air.toml 是可配置的。你可以定制它的参数以满足你的要求。

设置数据库

我们正在使用Gorm来设置我们的SQLite数据库驱动。Gorm也利用go-sqlite3包来设置SQLite数据库。

go-sqlite3 是 框架。 是Go编程语言中的一个模块,可以创建引用C代码的包。这意味着,如果你使用 构建你的Go应用程序,你需要GCC(GNU编译器集合)。CGo CGo go-sqlite3

GCC编译器是GNU项目的一个优化版本,可以编译各种编程语言,如C代码。

[go-sqlite3 文档]中指出 在你用go install github.com/mattn/go-sqlite3 (需要GCC)构建和安装go-sqlite3之后,你可以在将来不依赖GCC来构建你的应用程序。

然而,在我们的案例中,go-sqlite3 是通过Gorm SQLite驱动安装的。因此,我们需要建立一个GCC环境。让我们下载GCC MinGW-w64,用于32和64位Windows操作系统。然后,将MinGW-w64安装到你的笔记本电脑中。

在安装时,确保你选择以下架构。

gcc-for-windows

安装完成后,添加GCC环境变量。在系统变量部分找到PATH环境变量,添加GCC bin,即C:\Program Files\mingw-w64\x86_64-8.1.0-posix-seh-rt_v6-rev0\mingw64\bin

确保该路径与已安装的GCC的bin路径一致。为了应用这些变化,你需要重新启动你的计算机。一旦完成,在你的终端运行这个命令来检查这些变化是否被实施。

gcc

如果GCC被设置了,你会得到这个终端输出。

gcc-installed

我们现在准备建立我们的SQLite数据库。

在你的项目根目录下,创建一个文件夹并命名为database 。在这个文件夹中,创建一个database.go 文件。让我们开始编码。

一个database 模块,我们将用来导入到其他本地模块。该模块负责维护与数据库的连接和操作。

// the database module
package database

导入SQLite驱动和Gorm。这将负责创建我们的SQLite数据库。

// import packages
import (
    _ "gorm.io/driver/sqlite"
    "gorm.io/gorm"
)

为Gorm连接器创建一个数据库实例。

var (
    // Database instance => DB Gorm connector
    DBConn *gorm.DB
)

创建todos

在根目录下,创建一个todos 文件夹。在这个文件夹中,创建一个todos.go 文件。我们将用它来定义路由逻辑来处理数据的端到端处理。

CRUD操作是基于模型完成的,这有助于维护发送和接收客户端的数据的结构(以及它们之间的关系)。因此,我们将添加一个todo模型,维护发送和接收数据的结构。

创建一个todos模块

// the todos module
package todos

导入包和模块

import (
    // import modules
    "go-fiber-todos/database"
    "strconv"

    // import packages
    "github.com/gofiber/fiber/v2"
    "github.com/google/uuid"
    "gorm.io/gorm"
)

添加Todo结构

struct 是用来保存一个模型的集合的。在这种情况下,我们要设置和保持todos数据库的设置。我们主要对一个todo的 , 和完成的 感兴趣。ID Name status

Gorm正在帮助我们设置这个模型。通过Gorm,我们可以设置我们想要发展到数据库的数据结构。这有助于维护被发送和接收的数据的设计。

// Todo is a struct holding the todos settings.
type Todo struct {
    gorm.Model
    Id int `gorm:"primaryKey"`
    Name string `json:"name"`
    Completed bool `json:"completed"`
}

获取所有的todos

这里我们要创建一个GetAll() 方法。它将负责处理所有获取所有todos列表的请求。它以Fiber context为参数,帮助设置一个处理程序。一旦todos被获取,我们将以JSON格式返回。

// @ func GetAll -> function that fetches a single all todos (Get all todos)
// @param c *fiber.Ctx -- fiber context
func GetAll(c *fiber.Ctx) error {
    db := database.DBConn
    var todoss []Todo
    db.Find(&todoss)
    // If the database read is successful
    return c.Status(fiber.StatusOK).JSON(todoss)
}

基于id获取一个单独的todo

我们正在创建一个GetOne() ,只获取一个todo。在这种情况下,当设置这个处理程序时,我们使用todoid 作为参数。这意味着我们将把处理程序的端点创建为一个带有命名参数的路由。

Go Fiber将把这个参数映射到请求单个todo的端点。因此,例如,当设置路由处理GetOne() ,将解析一个todo特有的id ,然后返回与该id/参数相匹配的获取的数据。

在这种情况下,我们需要处理服务器发回的内容。如果id 匹配任何现有的todo,该todo将被返回到Fiber context并输出JSON格式。如果id 不存在,用户将得到一个错误todo not found

// @ func GetOne -> function that fetches a single todo (Get single todo)
// @param c *fiber.Ctx -- fiber context
func GetOne(ctx *fiber.Ctx) error {
    paramsId := ctx.Params("id")
    id, err := strconv.Atoi(paramsId)
    if err != nil {
        ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "cannot parse id",
        })
        return err
    }

    db := database.DBConn

    var todo Todo
    db.Find(&todo, id)

    // If the database read is successful
    if int(todo.Id) == id{
        return ctx.Status(fiber.StatusOK).JSON(todo)
    }

    // If the database fails to read the id parameter
    return ctx.Status(fiber.StatusNotFound).JSON(fiber.Map{
        "error": "todo not found",
    })
}

创建一个新的todo

下面代码中的AddTodo() 函数创建一个新的todo,并将其保存到数据库中。这里我们只添加NameCompleted 状态被默认设置为false

这就是uuid 开始发挥作用的地方。我们使用UUID实例来生成和检查我们的数据结构,然后再将其插入到数据库中。它生成了一个不可改变的通用唯一标识符(UUID)随机数。从而确保每个id对一个todo是唯一的。

// @func AddTodo -> function that stores a new data (Create new todo)
// @param c *fiber.Ctx -- fiber context
func AddTodo(ctx *fiber.Ctx) error {
    db := database.DBConn
    type request struct {
        Name string `json:"name"`
    }
    // Parse POST data
    var body request
    err := ctx.BodyParser(&body)
    if err != nil {
        ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "cannot parse json",
        })
        return err
    }
    // Get the json struct that is required to send
    id := uuid.New()
    todo := Todo{
        Id: int(id.ID()),
        Name: body.Name,
        Completed: false,
        }
    // Insert to DB
    db.Create(&todo)

    return ctx.Status(fiber.StatusOK).JSON(todo)
}

根据id删除一个todo

在下面的代码中,DeleteTodo() 将删除一个现有的todo。我们必须指定一个id作为Delete hanker和endpoint的参数。这定义了我们要删除的单一和独特的todo。

这里的id必须是一个现有的todo。否则,我们将返回一个错误。我们首先需要获取该todo,然后向数据库发送一个删除请求,删除与该todo相关的id。

// @func DeleteTodo -> a function that deletes the data (Delete todo)
// @param c *fiber.Ctx -- fiber context
func DeleteTodo(ctx *fiber.Ctx) error {
    db := database.DBConn
    paramsId := ctx.Params("id")
    id, err := strconv.Atoi(paramsId)
    if err != nil {
        return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "cannot parse id",
        })
    }

    var todo Todo
    db.First(&todo, id)

    if int(todo.Id) != id {
        return ctx.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "todo not found",
        })
    }

    db.Delete(&todo)

    return ctx.Status(fiber.StatusOK).JSON(fiber.Map{
        "status": "todo deleted successfully",
    })
}

更新一个现有的todo

在下面的代码中,UpdateTodo() 将更新一个现有todo的值。我们需要首先通过指定参数id来获取一个单独的todo。

在这里,我们可以改变todo的名称和todo的completed 值。完成的值会将todo更新为完整的,并有一个true

// @func UpdateTodo -> a function that ulters a todo data (Update todo)
// @param c *fiber.Ctx -- fiber context
func UpdateTodo(ctx *fiber.Ctx) error {
    db := database.DBConn

    type request struct {
        Name *string `json:"name"`
        Completed *bool `json:"completed"`
    }

    paramsId := ctx.Params("id")
    id, err := strconv.Atoi(paramsId)
    if err != nil {
        return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error": "cannot parse id",
        })
    }

    var body request

    err = ctx.BodyParser(&body)
    if err != nil {
        return ctx.Status(fiber.StatusBadRequest).JSON(fiber.Map{
            "error" : "Cannot parse body",
        })
    }

    var todo Todo
    // Check if todo exist, if exist assign it value to todo 
    db.First(&todo, id)

    // handling 404 error
    if int(todo.Id) != id {
        return ctx.Status(fiber.StatusNotFound).JSON(fiber.Map{
            "error": "todo not found",
        })
    }

    if body.Name != nil {
        todo.Name = *body.Name
    }

    if body.Completed != nil {
        todo.Completed = *body.Completed
    }

    db.Save(&todo)

    return ctx.Status(fiber.StatusOK).JSON(todo)
}

将处理程序分配给相应的路由

我们已经添加了所有的函数和处理程序,并为每个CRUD操作定义了所有的逻辑。这些处理程序是使用端点访问的。这些是URLs ,发送请求以执行对数据库的操作或向用户提供特定数据。我们将在main.go 文件中实现这一点。

创建一个todos模块

// the main module
package main

导入包和模块

在这里,我们要导入我们想要使用的本地、本地和第三方的包和模块。

import (
    // import packages
    "fmt"
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"

    // import modules
    "go-fiber-todos/database"
    "go-fiber-todos/todos"
)

设置Fiber应用程序

为了执行Go Fiber,我们需要设置一个Fiber应用程序。这里我们要添加Group ,它为我们的处理程序定义了路由。Group 也用于设置路由的共同前缀。在这种情况下,每个路径将有一个/v1 前缀。

// App config => App denotes the Fiber application.
func setupV1(app *fiber.App) {
    // Group is used for Routes with a common prefix to define a new sub-router with optional middleware.
    v1 := app.Group("/v1")
    //Each route will have /v1 prefix
    setupTodosRoutes(v1)
}

设置应用程序的路由

使用纤维Group ,让我们来设置我们的路由。每个路由将执行我们在todo.go 中设置的一个处理程序。所以我们在这里,添加简单路由和带参数的路由。

一个简单的路由除了设置"/" 端点外,不需要任何额外的参数。然而,一个带参数的路由有额外的参数,你需要通过这些参数来执行一个给定的端点。在我们的例子中,所有带参数的路由都把:id 作为额外参数。

// Router defines all router handle interface includes app and group router
func setupTodosRoutes(grp fiber.Router) {
    // Group is used for Routes with common prefix => Each route will have /todos prefix
    todosRoutes := grp.Group("/todos")
    // Route for Get all todos -> navigate to => http://127.0.0.1:3000/v1/todos/
    todosRoutes.Get("/", todos.GetAll)
    // Route for Get a todo -> navigate to => http://127.0.0.1:3000/v1/todos/<todo's id>
    todosRoutes.Get("/:id", todos.GetOne)
    // Route for Add a todo -> navigate to => http://127.0.0.1:3000/v1/todos/
    todosRoutes.Post("/", todos.AddTodo)
    // Route for Delete a todo -> navigate to => http://127.0.0.1:3000/v1/todos/<todo's id>
    todosRoutes.Delete("/:id", todos.DeleteTodo)
    // Route for Update a todo -> navigate to => http://127.0.0.1:3000/v1/todos/<todo's id>
    todosRoutes.Patch("/:id", todos.UpdateTodo)
}

初始化数据库

我们设置了数据库,但我们没有初始化或创建SQLite数据库。这里我们需要创建一个todos.db 文件来保存我们的todo。

我们也在迁移设定的数据库结构。所有这些都将在我们建立应用程序时自动创建。

// Database Connect function
func initDatabase() {
    // define error here to prevent overshadowing the global DB
    var err error
    // Create todos sqlite file & Config GORM config
    // GORM performs single create, update, delete operations in transactions by default to ensure database data integrity
    database.DBConn, err = gorm.Open(sqlite.Open("todos.db"), &gorm.Config{})

    // Connect to database
    if err != nil {
        // Database was connected
        panic("failed to connect database")
    }

    fmt.Println("Database successfully connected")

    // AutoMigrate run auto migration for gorm model
    database.DBConn.AutoMigrate(&todos.Todo{})
    // Initialize Database connection
    fmt.Println("Database Migrated")
}

定义应用程序的入口点

这将定义我们的应用程序入口点。

我们正在执行以下操作。

  • 实例化一个新的Fiber App。
  • 调用initDatabase() 方法。
  • 调用setupV1(app) 方法。
  • 为一个简单的路由设置一个中间件函数,返回纯文本。
  • 设置一个记录器中间件。这个中间件将被用来记录HTTP动词。GET、POST、PUT,等等。对于每个路由,当每个HTTP动词被调用时。
  • 添加一个服务器端口号。这将被用来在localhost上运行我们的服务器。
  • 处理panic 错误。panic 是Go内置的一个函数,它监视所有函数的执行。如果一个函数出现错误,正常的执行会立即停止。
// entry point to our program
func main() {
    // call the New() method - used to instantiate a new Fiber App
    app := fiber.New()

    // call the initDatabase() method
    initDatabase()
    // call the setupV1(app) method
    setupV1(app)

    // Simple route => Middleware function
    app.Get("/", func(c *fiber.Ctx) error {
        // Returns plain text.
        return c.SendString("Hello, World!")
        // navigate to => http://127.0.0.1:3000
    })

    // sets up logger
    // Use middlewares for each route
    // This method will match all HTTP verbs: GET, POST, PUT etc Then create a log when every HTTP verb get invoked
    app.Use(logger.New(logger.Config{ // add Logger middleware with config
        Format: "[${ip}]:${port} ${status} - ${method} ${path}\n",
    }))

    // listen/Serve the new Fiber app on port 3000
    err := app.Listen(":3000")

    // handle panic errors => panic built-in function that stops the execution of a function and immediately normal execution of that function with an error
    if err != nil {
        panic(err)
    }
}

todo应用程序已经设置好了,可以进行测试了。

测试

我们将使用air 命令来启动服务器,也就是说,在你的项目根目录下运行以下命令。

air

这将构建一个可执行文件并将其保存在temp 文件夹中。下面是命令的输出。

air-server

有了air ,观察和构建我们的服务器就变得很容易了。如果你改变了任何代码,服务器将重新加载、构建并重新运行。首先,air 将删除tmp 的可执行文件,重建应用程序,并保存新构建的可执行文件。

air-live-reloaded-server

现在让我们测试一下我们的端点。同样,我们将使用Postman来测试这些端点。

打开你的Postman,向http://127.0.0.1:3000 发送一个GET请求,如下图所示。

postman-get-request

现在让我们测试一下todo路由。

  • 添加一个新的todo。

让我们先在数据库中添加一个todos的列表。下面是一个我想插入到SQLite数据库的todo列表样本。接下来,导航到Postman并执行一个post请求,如下图所示。

add-a-new-todo

新的todo如下图所示。

the-newly-added-todo

如果你得到的东西与你添加的东西不同,你可能忘记了一两个步骤。重新审视你的AddTodo() 函数,或者检查一下你使用的JSON数据是否有良好的格式化。

另外,继续添加一些其他的todos。

  • 取出todos。

现在让我们来获取添加的todos。这里将执行一个获取请求,如下图所示。

postman-get-request

fetched-todos

  • 获取一个单独的todo。

要获取单个todo,你需要在你的URL中指定todo的id作为一个参数。

看看这个例子。

single-todo

  • 更新一个todo。

一旦一个todo被添加,我们可以执行更新操作来改变该todo的值。在这里,我们必须指定我们想要更新的todo的ID。然后添加你想替换todo的数据。

update-a-todo

一旦你发送一个PATCH请求,分配给该todo的id的值就会更新。

下面是一个例子。

updated-todo

  • 删除一个todo。

现在让我们执行最后一项操作,删除一个现有的todo。让我们指定你要删除的todo的id,如下图所示。

todo-deleted

如果你试图向被删除的todo发送一个GET请求,你应该得到一个错误提示" error": "todo not found"

deleted-todo-not-found

总结

Golang是一种神奇的语言。你可以创建几乎任何你在其他语言中可以创建的应用程序。此外,Go可以处理大量的应用程序。因此,它可以建立各种级别的应用程序,同时由于其利用多核处理的能力而确保最小化。