从头开始使用 Golang 创建 Restful API
在本教程中,我们将通过一个示例,讲解如何使用 Golang 创建 Restful API。我们将介绍应用程序的初始设置、应用程序的架构、持久化处理、Docker 配置、HTTP 请求,甚至使用 vscode 进行调试。
本教程涵盖了从应用程序设置基础开始的诸多概念,因此我建议您查看下方的目录,确定如何学习本教程,并跳转到您更感兴趣的部分。
目录
- 1.为什么选择 Golang
- 2.初始设置
- 3.使用光纤创建您的第一个端点
- 4.使用 Docker 运行
- 4.1.设置 Dockerfile
- 4.2.包含 Air 以实现热重载
- 4.3.创建 docker-compose.yaml 配置
- 5.设置 VSCode 调试器
- 6.创建 Todo API
- 构建和测试
- 失眠测试
- 8.1.导入 Swagger
- 8.2.测试创建Todo
- 8.3.测试获取Todo集合
- 8.4.测试获取单个待办事项
- 8.5.测试更新Todo
- 8.6.测试删除 Todo
为什么选择 Golang?
Golang 是一种极其轻量且快速的语言,可用于构建 CLI、DevOps 和 SRE、Web 开发、分布式服务、数据库实现等等。例如,Docker 和 Kubernetes 等应用程序都是用 Go 编写的。Go
以其性能、易学性(仅 25 个保留关键字)和改进现有代码库而闻名。
初始设置
创建文件夹并初始化应用程序
mkdir go-todo
cd go-todo
git init
Go 有自己的包管理器,通过 go.mod 文件进行处理。要生成此基础文件,您可以运行以下命令:
go mod init <YOUR API NAME>
你可以将 替换<YOUR API NAME>
为你想要的任何应用名称,但请记住,惯例是使用你的应用可用的远程存储库的名称。
例如 github.com/pachecoio/go-todo
go mod init github.com/pachecoio/go-todo
这一点尤其重要,因为这就是 Go 管理依赖项的方式。Go 不会将依赖项下载到你的本地项目目录,而是通过模块以分散的方式处理它们,我们将在接下来的章节中看到这一点。
如果你想深入了解这个概念,可以查看这里了解更多信息。
现在让我们为这个应用程序创建入口点文件。
touch main.go # Create a main.go file
code . # Open with vscode
让我们包含一些样板代码来测试我们的初始设置:
package main
import "fmt"
func main() {
fmt.Println("App running")
}
您可以通过go main.go
在终端中运行该应用程序,或者如果您使用 vscode,请按 F5 来运行该应用程序。
使用光纤创建您的第一个端点
在这个例子中,我们将使用 Fiber 来处理我们的 HTTP 请求并创建 API 的基本结构。
Fiber是一个使用 Fasthttp 构建的 Web 框架,其灵感来自 Express,旨在简化快速开发,零内存分配,并考虑性能。
首先,让我们获取光纤依赖项:
go get github.com/gofiber/fiber/v2
现在我们可以将 fiber 模块导入到 main.go 文件中,并替换 main 函数内的代码。
package main
import (
"log"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
)
func main() {
app := fiber.New()
app.Use(cors.New())
api := app.Group("/api")
// Test handler
api.Get("/", func(c *fiber.Ctx) error {
return c.SendString("App running")
})
log.Fatal(app.Listen(":5000"))
}
让我们来看看这里发生的事情:
首先,我们可以看到 Go 使用远程仓库的 URL 来导入依赖项/模块。
接下来,我们创建一个新的 Fiber 实例,其中包含一系列辅助函数,其主要功能是监听文件末尾指定的 5000 端口上的 HTTP 请求。
变量api
是一个分组示例,可用于组织端点路径。在此示例中,我们使用了/api
扩展名,这意味着我们访问端点的基本 URL 将是http://localhost:5000/api
。
此时,您可以再次运行该应用程序并尝试发出请求http://localhost:5000/api
并接收以下响应:
App running
您只需在浏览器中输入此 URL 即可进行测试,但我建议使用 HTTP 客户端(如Insomnia),因为本课的其余部分也需要它。
使用 Docker 运行
此时,我们已经可以运行应用程序并通过按 F5 使用 vscode 进行调试,但是随着依赖项和复杂性的增加,我们越来越难以获得可用于生产环境的应用程序,因为我们过于依赖本地设置(尤其是在开始添加数据库时)。
因此,一个好的解决方案是使用 Docker 将此应用程序容器化。
如果您不想使用 Docker 运行此操作,可以跳过此部分,但请记住,对于以下部分,您需要启动并运行 Postgres 数据库才能继续本教程。
设置 Dockerfile
第一步是创建 Dockerfile:
FROM golang:1.16-alpine AS base
WORKDIR /app
ENV GO111MODULE="on"
ENV GOOS="linux"
ENV CGO_ENABLED=0
RUN apk update \
&& apk add --no-cache \
ca-certificates \
curl \
tzdata \
git \
&& update-ca-certificates
FROM base AS dev
WORKDIR /app
RUN go get -u github.com/cosmtrek/air && go install github.com/go-delve/delve/cmd/dlv@latest
EXPOSE 5000
EXPOSE 2345
ENTRYPOINT ["air"]
FROM base AS builder
WORKDIR /app
COPY . /app
RUN go mod download \
&& go mod verify
RUN go build -o todo -a .
FROM alpine:latest as prod
COPY --from=builder /app/todo /usr/local/bin/todo
EXPOSE 5000
ENTRYPOINT ["/usr/local/bin/todo"]
让我们来看看这里发生的事情:如果您已经熟悉 Docker,
可以跳过本节。
- 首先,我们引用基础图像
golang:1.16-alpine
并设置一个名为的别名base
。 - 第 4 至 6 行我们设置了一些基本环境变量:
GO111MODULE=on
即使项目位于 GOPATH 中,也强制使用模块。需要 go.mod 才能运行。GOOS=linux
告诉编译器需要为哪个操作系统构建。CGO_ENABLED=0
允许程序使用静态链接的二进制文件进行构建,而无需任何外部依赖。
- 第 8 至 14 行用于更新本地依赖项并安装 ca 证书(如果您打算使用 SSL/TLS,这一点很重要)
dev
第 16 行我们根据base
舞台创建一个舞台- 第 19 行我们安装air,用于热重载
- 第 20 行和 21 行我们公开主端口 5000 和调试端口 2345。
-
第 23 行是 dev 阶段的入口点,它基本上运行
air
-
接下来,从第 25 行到第 32 行,我们创建一个构建器阶段,用于为生产运行创建已编译的应用程序。在此步骤中,我们基本上复制所有代码,安装依赖项并编译应用程序。
-
最后但并非最不重要的一点是,从第 34 行到第 39 行,我们设置了我们的产品配置,我们从构建器阶段提取编译后的代码,公开端口 5000 并为这个编译后的应用程序设置入口点。
包括用于热重载的空气
我们将使用air来实现 dev/debug 模式下的热重载。Dockerfile 已经考虑到了这一点,但我们仍然需要添加 air 的配置。
让我们创建一个名为 .air.toml 的新文件,并包含以下内容:
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -gcflags='all=-N -l' -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary.
full_bin = "dlv exec --accept-multiclient --log --headless --continue --listen :2345 --api-version 2 ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
创建 docker-compose.yaml 配置
现在让我们添加一个 docker-compose.yaml 文件,以便我们可以轻松地持续构建、运行和定制这个应用程序。
# docker-compose.yaml
version: "3.7"
services:
go-todo:
container_name: go-todo
image: thisk8brd/go-todo:dev
build:
context: .
target: dev
volumes:
- .:/app
ports:
- "5000:5000"
- "2345:2345"
networks:
- go-todo-network
networks:
go-todo-network:
name: go-todo-network
太棒了!
现在,你应该能够使用 docker 运行,并获得与上一节相同的结果。
要运行该应用程序,请在终端中执行以下命令:
docker compose up
再次访问http://localhost:5000/api
,应用程序应该就能正常运行了。
你还可以修改文件,并在终端日志中看到热重载的反馈。
设置 VSCode 调试器
应用程序运行起来并查看日志已经很不错了,但如果我们能在容器内使用 vscode 调试功能就更好了。所以我们来添加一下:
在项目的根目录中,创建一个名为 .vscode 的文件夹,并在其中添加一个名为 launch.json 的文件,其内容如下:
// .vscode/launch/json
{
"version": "0.2.0",
"configurations": [
{
"name": "Delve into Docker",
"type": "go",
"request": "attach",
"mode": "remote",
"substitutePath": [
{
"from": "${workspaceFolder}/",
"to": "/app/"
}
],
"port": 2345,
"host": "127.0.0.1",
"showLog": true,
"apiVersion": 2,
"trace": "verbose"
}
]
}
现在,由于您的应用程序已经在运行,您只需按 F5,VSCode 调试器就会附加到您正在运行的容器中。
您可以在端点处理程序内设置一个断点,尝试访问http://localhost:5000/api
并查看其是否正常工作。
创建 Todo API
现在我们已经完成了初始设置并运行,可以开始实现 API 了。
在本例中,我们将创建一个 Todo API,其中包含 Todo 实体的基本 CRUD 操作。
包括数据库连接
我们将使用 Postgres 及其依赖项来建立gorm
连接。
那就让我们开始编写代码吧!
首先,让我们创建一个.env
包含我们将要使用的数据库的凭据的文件:
DB_HOST=go-todo-db
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=root
DB_NAME=go-todo-db
go-todo
现在,让我们使用以下代码在我们的服务之后立即将新服务包含到我们的 docker-compose.yaml 文件中:
go-todo-db:
container_name: go-todo-db
image: postgres
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
volumes:
- postgres-db:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- go-todo-network
完成此服务后,让我们添加一个名为的新卷postgres-db
:
volumes:
postgres-db:
完整的 docker-compose.yaml 文件应如下所示:
version: "3.7"
services:
go-todo:
container_name: go-todo
image: thisk8brd/go-todo:dev
build:
context: .
target: dev
volumes:
- .:/app
ports:
- "5000:5000"
- "2345:2345"
networks:
- go-todo-network
go-todo-db:
container_name: go-todo-db
image: postgres
environment:
- POSTGRES_USER=${DB_USER}
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=${DB_NAME}
volumes:
- postgres-db:/var/lib/postgresql/data
ports:
- "5432:5432"
networks:
- go-todo-network
volumes:
postgres-db:
networks:
go-todo-network:
name: go-todo-network
创建一个名为 config 的文件夹,并config.go
在其中添加一个文件,内容如下:
// config/config.go
package config
import (
"fmt"
"os"
"github.com/joho/godotenv"
)
func Config(key string) string {
err := godotenv.Load(".env")
if err != nil {
fmt.Print("Error loading .env file")
}
return os.Getenv(key)
}
此代码将负责加载我们刚刚创建的 .env 内容。
创建一个名为 database 的模块,并在其中创建两个文件:connect.go 和 database.go。
在database.go文件中包含以下内容:
// database/database.go
package database
import "github.com/jinzhu/gorm"
// DB gorm connector
var DB *gorm.DB
接下来,在 connect.go 文件中包含以下内容:
// database/connect.go
package database
import (
"fmt"
"strconv"
"github.com/pachecoio/go-todo/config"
"github.com/jinzhu/gorm"
_ "github.com/lib/pq"
)
func ConnectDB() {
var err error
p := config.Config("DB_PORT")
port, err := strconv.ParseUint(p, 10, 32)
configData := 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"),
)
DB, err = gorm.Open(
"postgres",
configData,
)
if err != nil {
fmt.Println(
err.Error(),
)
panic("failed to connect database")
}
fmt.Println("Connection Opened to Database")
}
Todo模块实现
现在让我们创建我们的待办事项模块:
创建一个名为的新文件夹todo
并创建一个名为 models.go 的文件:
// todo/models.go
package todo
import "github.com/jinzhu/gorm"
const (
PENDING = "pending"
PROGRESS = "in_progress"
DONE = "done"
)
type Todo struct {
gorm.Model
Name string `gorm:"Not Null" json:"name"`
Description string `json:"description"`
Status string `gorm:"Not Null" json:"status"`
}
接下来,让我们创建存储库,它将负责处理刚刚创建的 Todo 模型的所有数据库交互。
在 todo 模块中创建一个名为 repositories.go 的文件:
// todo/repositories.go
package todo
import (
"errors"
"github.com/jinzhu/gorm"
)
type TodoRepository struct {
database *gorm.DB
}
func (repository *TodoRepository) FindAll() []Todo {
var todos []Todo
repository.database.Find(&todos)
return todos
}
func (repository *TodoRepository) Find(id int) (Todo, error) {
var todo Todo
err := repository.database.Find(&todo, id).Error
if todo.Name == "" {
err = errors.New("Todo not found")
}
return todo, err
}
func (repository *TodoRepository) Create(todo Todo) (Todo, error) {
err := repository.database.Create(&todo).Error
if err != nil {
return todo, err
}
return todo, nil
}
func (repository *TodoRepository) Save(user Todo) (Todo, error) {
err := repository.database.Save(user).Error
return user, err
}
func (repository *TodoRepository) Delete(id int) int64 {
count := repository.database.Delete(&Todo{}, id).RowsAffected
return count
}
func NewTodoRepository(database *gorm.DB) *TodoRepository {
return &TodoRepository{
database: database,
}
}
此存储库文件提供了此待办事项模型的基本 CRUD 操作,以及一个用于实例化的辅助函数。接下来我们将介绍如何使用它。
现在让我们创建端点处理程序。
在 todo 模块中创建一个名为 handlers.go 的新文件。
// todo/handlers.go
package todo
import (
"strconv"
"github.com/gofiber/fiber/v2"
"github.com/jinzhu/gorm"
)
type TodoHandler struct {
repository *TodoRepository
}
func (handler *TodoHandler) GetAll(c *fiber.Ctx) error {
var todos []Todo = handler.repository.FindAll()
return c.JSON(todos)
}
func (handler *TodoHandler) Get(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
todo, err := handler.repository.Find(id)
if err != nil {
return c.Status(404).JSON(fiber.Map{
"status": 404,
"error": err,
})
}
return c.JSON(todo)
}
func (handler *TodoHandler) Create(c *fiber.Ctx) error {
data := new(Todo)
if err := c.BodyParser(data); err != nil {
return c.Status(500).JSON(fiber.Map{"status": "error", "message": "Review your input", "error": err})
}
item, err := handler.repository.Create(*data)
if err != nil {
return c.Status(400).JSON(fiber.Map{
"status": 400,
"message": "Failed creating item",
"error": err,
})
}
return c.JSON(item)
}
func (handler *TodoHandler) Update(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
return c.Status(400).JSON(fiber.Map{
"status": 400,
"message": "Item not found",
"error": err,
})
}
todo, err := handler.repository.Find(id)
if err != nil {
return c.Status(404).JSON(fiber.Map{
"message": "Item not found",
})
}
todoData := new(Todo)
if err := c.BodyParser(todoData); err != nil {
return c.Status(400).JSON(fiber.Map{"status": "error", "message": "Review your input", "data": err})
}
todo.Name = todoData.Name
todo.Description = todoData.Description
todo.Status = todoData.Status
item, err := handler.repository.Save(todo)
if err != nil {
return c.Status(400).JSON(fiber.Map{
"message": "Error updating todo",
"error": err,
})
}
return c.JSON(item)
}
func (handler *TodoHandler) Delete(c *fiber.Ctx) error {
id, err := strconv.Atoi(c.Params("id"))
if err != nil {
return c.Status(400).JSON(fiber.Map{
"status": 400,
"message": "Failed deleting todo",
"err": err,
})
}
RowsAffected := handler.repository.Delete(id)
statusCode := 204
if RowsAffected == 0 {
statusCode = 400
}
return c.Status(statusCode).JSON(nil)
}
func NewTodoHandler(repository *TodoRepository) *TodoHandler {
return &TodoHandler{
repository: repository,
}
}
func Register(router fiber.Router, database *gorm.DB) {
database.AutoMigrate(&Todo{})
todoRepository := NewTodoRepository(database)
todoHandler := NewTodoHandler(todoRepository)
movieRouter := router.Group("/todo")
movieRouter.Get("/", todoHandler.GetAll)
movieRouter.Get("/:id", todoHandler.Get)
movieRouter.Put("/:id", todoHandler.Update)
movieRouter.Post("/", todoHandler.Create)
movieRouter.Delete("/:id", todoHandler.Delete)
}
此文件负责处理 HTTP 请求。
我们提供了一组函数来获取所有 Todo、获取单个 Todo、更新、创建和删除 Todo。
我们还提供了一个助手来注册此处理程序,它还涵盖了自动迁移数据库的功能,因此我们无需自行创建表。
总结
现在在我们的 main.go 文件中,让我们连接到数据库并注册我们的待办事项处理程序:
在 CORS 定义之后立即初始化 DB 连接:
// main.go
// ...
app.Use(cors.New())
database.ConnectDB()
defer database.DB.Close()
删除我们现有的测试端点并注册新的待办事项处理程序:
todo.Register(api, database.DB)
完整的 main.go 文件应如下所示:
// main.go
package main
import (
"log"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/cors"
"github.com/pachecoio/go-todo/database"
"github.com/pachecoio/go-todo/todo"
)
func main() {
app := fiber.New()
app.Use(cors.New())
database.ConnectDB()
defer database.DB.Close()
api := app.Group("/api")
todo.Register(api, database.DB)
log.Fatal(app.Listen(":5000"))
}
此时,我们可以再次运行应用程序并测试端点。运行以下命令,使用 docker 重建并运行应用程序:
docker compose up --build
我们传递该
--build
标志是为了确保重新构建并安装刚刚添加的依赖项,但您不需要每次运行时都包含此标志。仅当您更改任何依赖项或 Dockerfile 本身时才需要包含它。
按 F5 连接 VSCode 调试器并测试应用程序!
您可以使用此 swagger文件来检查和测试我们刚刚为此应用创建的端点。
为了测试端点,我建议您下载一个 HTTP 客户端,例如Insomnia。请按照以下步骤进行测试:
失眠测试
创建新的设计文档
转到“设计”选项卡,复制 Swagger 内容并粘贴到编辑器中
转到调试部分并检查生成的端点
测试创建待办事项端点
测试 Todo 集合端点
测试获取单个待办事项端点
测试更新待办事项端点
测试删除待办事项端点
我鼓励您添加其他功能,例如按状态和关键字过滤待办事项、验证允许的状态等等。请在评论区分享您的成果!
希望本教程对您有所帮助,如果您有任何疑问或建议,欢迎随时提出!源代码可在此处
找到。