迷你指南 - 与 MySQL 一起构建 REST API 作为 Go 微服务设置 API 数据库连接结构和依赖关系数据库迁移包装 REST API

2025-05-25

迷你指南 — 使用 Go 和 MySQL 构建 REST API 作为微服务

设置 API

数据库连接

结构与依赖

数据库迁移

总结

REST API

最近,我发现自己在 Storytel 的日常工作和我的业余项目Wiseer中编写并部署了大量 Go 微服务。在本小教程中,我将使用 MySQL 数据库创建一个简单的 REST-API。完整项目的代码链接将在文章末尾提供。

如果您还没有这样做,我建议您参加Go Tour作为本文的补充。

让我们开始吧!


设置 API

首先要做的事情之一是选择一个路由包。路由的作用是将 URL 连接到可执行函数。mux对我来说是一个不错的路由包,但也有其他选择,性能上并无太大区别,例如httprouterchi。在本指南中,我将使用 mux 包。

为了简单起见,我们将创建一个打印消息的单个端点。

package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
func setupRouter(router *mux.Router) {
router.
Methods("POST").
Path("/endpoint").
HandlerFunc(postFunction)
}
func postFunction(w http.ResponseWriter, r *http.Request) {
log.Println("You called a thing!")
}
func main() {
router := mux.NewRouter().StrictSlash(true)
setupRouter(router)
log.Fatal(http.ListenAndServe(":8080", router))
}
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
func setupRouter(router *mux.Router) {
router.
Methods("POST").
Path("/endpoint").
HandlerFunc(postFunction)
}
func postFunction(w http.ResponseWriter, r *http.Request) {
log.Println("You called a thing!")
}
func main() {
router := mux.NewRouter().StrictSlash(true)
setupRouter(router)
log.Fatal(http.ListenAndServe(":8080", router))
}

上面的代码创建了一个Router,将一个 URL 与一个处理函数(在我们的例子中是 postFunction )关联起来,并使用该 Router 在端口 8080 上启动一个服务器。

很简单吧?🤠


数据库连接

让我们基于上面的代码,将其连接到 MySQL 数据库。Go 提供了SQL 数据库的接口,但需要驱动程序。在本例中,我使用了go-sql-driver 。

package db
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func CreateDatabase() (*sql.DB, error) {
serverName := "localhost:3306"
user := "myuser"
password := "pw"
dbName := "demo"
connectionString := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=true&multiStatements=true", user, password, serverName, dbName)
db, err := sql.Open("mysql", connectionString)
if err != nil {
return nil, err
}
return db, nil
}
package db
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
func CreateDatabase() (*sql.DB, error) {
serverName := "localhost:3306"
user := "myuser"
password := "pw"
dbName := "demo"
connectionString := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&collation=utf8mb4_unicode_ci&parseTime=true&multiStatements=true", user, password, serverName, dbName)
db, err := sql.Open("mysql", connectionString)
if err != nil {
return nil, err
}
return db, nil
}

代码放在另一个名为db的包中,并假设在localhost:3306上运行着一个名为demo的数据库。返回的数据库会自动处理到该数据库的连接池。

让我们更新前面代码片段中的postFunction ,以使用这个数据库。

func postFunction(w http.ResponseWriter, r *http.Request) {
database, err := db.CreateDatabase()
if err != nil {
log.Fatal("Database connection failed")
}
_, err = database.Exec("INSERT INTO `test` (name) VALUES ('myname')")
if err != nil {
log.Fatal("Database INSERT failed")
}
log.Println("You called a thing!")
}
view raw post_func.go hosted with ❤ by GitHub
func postFunction(w http.ResponseWriter, r *http.Request) {
database, err := db.CreateDatabase()
if err != nil {
log.Fatal("Database connection failed")
}
_, err = database.Exec("INSERT INTO `test` (name) VALUES ('myname')")
if err != nil {
log.Fatal("Database INSERT failed")
}
log.Println("You called a thing!")
}
view raw post_func.go hosted with ❤ by GitHub

真的就这么简单!其实很简单,但上面的代码有一些问题,而且缺少了一些锦上添花的功能。接下来会有点棘手,但先别急着放弃!⚓️


结构与依赖

如果你仔细检查过上面的代码,你可能会注意到,我们在每次 API 调用时都会打开数据库;即使打开的数据库可以安全地并发使用。我们需要一些依赖管理来确保只打开数据库一次,为此,我们需要使用struct

package app
import (
"database/sql"
"log"
"net/http"
"github.com/gorilla/mux"
)
type App struct {
Router *mux.Router
Database *sql.DB
}
func (app *App) SetupRouter() {
app.Router.
Methods("POST").
Path("/endpoint").
HandlerFunc(app.postFunction)
}
func (app *App) postFunction(w http.ResponseWriter, r *http.Request) {
_, err := app.Database.Exec("INSERT INTO `test` (name) VALUES ('myname')")
if err != nil {
log.Fatal("Database INSERT failed")
}
log.Println("You called a thing!")
w.WriteHeader(http.StatusOK)
}
view raw app.go hosted with ❤ by GitHub
package app
import (
"database/sql"
"log"
"net/http"
"github.com/gorilla/mux"
)
type App struct {
Router *mux.Router
Database *sql.DB
}
func (app *App) SetupRouter() {
app.Router.
Methods("POST").
Path("/endpoint").
HandlerFunc(app.postFunction)
}
func (app *App) postFunction(w http.ResponseWriter, r *http.Request) {
_, err := app.Database.Exec("INSERT INTO `test` (name) VALUES ('myname')")
if err != nil {
log.Fatal("Database INSERT failed")
}
log.Println("You called a thing!")
w.WriteHeader(http.StatusOK)
}
view raw app.go hosted with ❤ by GitHub

我们首先创建一个名为app的新包,用于托管我们的结构体及其方法。我们的App结构体包含两个字段:一个Router和一个Database ,分别在第 17 行第 24 行访问。我们还在第 30 行方法的末尾手动设置了返回的状态码

为了使用新的App结构体,main 包和函数也需要进行一些修改。我们将postFunctionsetupRouter函数从 main 包中移除,因为它们现在位于 app 包中。现在剩下:

package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/johan-lejdung/go-microservice-api-guide/internal/app"
"github.com/johan-lejdung/go-microservice-api-guide/internal/db"
)
func main() {
database, err := db.CreateDatabase()
if err != nil {
log.Fatal("Database connection failed: %s", err.Error())
}
app := &app.App{
Router: mux.NewRouter().StrictSlash(true),
Database: database,
}
app.SetupRouter()
log.Fatal(http.ListenAndServe(":8080", app.Router))
}
view raw struct_main.go hosted with ❤ by GitHub
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
"github.com/johan-lejdung/go-microservice-api-guide/internal/app"
"github.com/johan-lejdung/go-microservice-api-guide/internal/db"
)
func main() {
database, err := db.CreateDatabase()
if err != nil {
log.Fatal("Database connection failed: %s", err.Error())
}
app := &app.App{
Router: mux.NewRouter().StrictSlash(true),
Database: database,
}
app.SetupRouter()
log.Fatal(http.ListenAndServe(":8080", app.Router))
}
view raw struct_main.go hosted with ❤ by GitHub

为了使用我们的新结构体,我们打开一个数据库和一个新的Router 。然后将它们都插入到新的App结构体的字段中

恭喜!现在您已连接到数据库,并且所有传入的 API 调用都将同时使用该连接。

最后一步,我们将在路由器设置中添加一个GET方法,并以JSON 格式返回数据。首先添加一个结构体来填充数据,然后将字段映射到JSON 格式

package app
import (
"time"
)
type DbData struct {
ID int `json:"id"`
Date time.Time `json:"date"`
Name string `json:"name"`
}
view raw dbdata.go hosted with ❤ by GitHub
package app
import (
"time"
)
type DbData struct {
ID int `json:"id"`
Date time.Time `json:"date"`
Name string `json:"name"`
}
view raw dbdata.go hosted with ❤ by GitHub

接下来,我们扩展了app.go文件,添加了一个新的方法getFunction,用于获取数据并将其写入客户端响应。最终文件如下所示。

package app
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
)
type App struct {
Router *mux.Router
Database *sql.DB
}
func (app *App) SetupRouter() {
app.Router.
Methods("GET").
Path("/endpoint/{id}").
HandlerFunc(app.getFunction)
app.Router.
Methods("POST").
Path("/endpoint").
HandlerFunc(app.postFunction)
}
func (app *App) getFunction(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, ok := vars["id"]
if !ok {
log.Fatal("No ID in the path")
}
dbdata := &DbData{}
err := app.Database.QueryRow("SELECT id, date, name FROM `test` WHERE id = ?", id).Scan(&dbdata.ID, &dbdata.Date, &dbdata.Name)
if err != nil {
log.Fatal("Database SELECT failed")
}
log.Println("You fetched a thing!")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(dbdata); err != nil {
panic(err)
}
}
func (app *App) postFunction(w http.ResponseWriter, r *http.Request) {
_, err := app.Database.Exec("INSERT INTO `test` (name) VALUES ('myname')")
if err != nil {
log.Fatal("Database INSERT failed")
}
log.Println("You called a thing!")
w.WriteHeader(http.StatusOK)
}
view raw app.go hosted with ❤ by GitHub
package app
import (
"database/sql"
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
)
type App struct {
Router *mux.Router
Database *sql.DB
}
func (app *App) SetupRouter() {
app.Router.
Methods("GET").
Path("/endpoint/{id}").
HandlerFunc(app.getFunction)
app.Router.
Methods("POST").
Path("/endpoint").
HandlerFunc(app.postFunction)
}
func (app *App) getFunction(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, ok := vars["id"]
if !ok {
log.Fatal("No ID in the path")
}
dbdata := &DbData{}
err := app.Database.QueryRow("SELECT id, date, name FROM `test` WHERE id = ?", id).Scan(&dbdata.ID, &dbdata.Date, &dbdata.Name)
if err != nil {
log.Fatal("Database SELECT failed")
}
log.Println("You fetched a thing!")
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(dbdata); err != nil {
panic(err)
}
}
func (app *App) postFunction(w http.ResponseWriter, r *http.Request) {
_, err := app.Database.Exec("INSERT INTO `test` (name) VALUES ('myname')")
if err != nil {
log.Fatal("Database INSERT failed")
}
log.Println("You called a thing!")
w.WriteHeader(http.StatusOK)
}
view raw app.go hosted with ❤ by GitHub

数据库迁移

我们将为项目添加最后一个功能。当数据库与应用程序或服务紧密耦合时,通过妥善处理该数据库的迁移,可以避免难以想象的麻烦。我们将使用migration来实现这一点,并扩展我们的db 包

我将在嵌入代码下方详细阐述以下内容,虽然很长。

package db
import (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
"github.com/golang-migrate/migrate"
"github.com/golang-migrate/migrate/database/mysql"
_ "github.com/golang-migrate/migrate/source/file"
)
func CreateDatabase() (*sql.DB, error) {
// I shortened the code here. Here is where the DB setup were made.
// In order to save some space I've removed the connection setup, but it can
// be seen here: https://gist.github.com/johan-lejdung/ecea9dab9b9621d0ceb054cec70ae676#file-database_connect-go
if err := migrateDatabase(db); err != nil {
return db, err
}
return db, nil
}
func migrateDatabase(db *sql.DB) error {
driver, err := mysql.WithInstance(db, &mysql.Config{})
if err != nil {
return err
}
dir, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
migration, err := migrate.NewWithDatabaseInstance(
fmt.Sprintf("file://%s/internal/db/migrations", dir),
"mysql",
driver,
)
if err != nil {
return err
}
migration.Log = &MigrationLogger{}
migration.Log.Printf("Applying database migrations")
err = migration.Up()
if err != nil && err != migrate.ErrNoChange {
return err
}
version, _, err := migration.Version()
if err != nil {
return err
}
migration.Log.Printf("Active database version: %d", version)
return nil
}
package db
import (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/go-sql-driver/mysql"
"github.com/golang-migrate/migrate"
"github.com/golang-migrate/migrate/database/mysql"
_ "github.com/golang-migrate/migrate/source/file"
)
func CreateDatabase() (*sql.DB, error) {
// I shortened the code here. Here is where the DB setup were made.
// In order to save some space I've removed the connection setup, but it can
// be seen here: https://gist.github.com/johan-lejdung/ecea9dab9b9621d0ceb054cec70ae676#file-database_connect-go
if err := migrateDatabase(db); err != nil {
return db, err
}
return db, nil
}
func migrateDatabase(db *sql.DB) error {
driver, err := mysql.WithInstance(db, &mysql.Config{})
if err != nil {
return err
}
dir, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
migration, err := migrate.NewWithDatabaseInstance(
fmt.Sprintf("file://%s/internal/db/migrations", dir),
"mysql",
driver,
)
if err != nil {
return err
}
migration.Log = &MigrationLogger{}
migration.Log.Printf("Applying database migrations")
err = migration.Up()
if err != nil && err != migrate.ErrNoChange {
return err
}
version, _, err := migration.Version()
if err != nil {
return err
}
migration.Log.Printf("Active database version: %d", version)
return nil
}

在打开数据库后,我们立即向migrateDatabase添加另一个函数调用,该函数将依次启动迁移过程。

我们还将添加一个 MigrationLogger 结构来处理过程中的日志记录,代码可以在这里看到,它的用法在第 45 行

迁移是通过常规 SQL 查询执行的。迁移文件从第 37 行的文件夹中读取。

每次打开数据库时,所有未应用的数据库迁移都将被应用。从而无需任何手动干预即可保持数据库更新。

这与包含数据库的docker-compose文件相结合,使得在多台机器上进行开发变得非常简单。


总结

所以你已经一路走到这里了👏👏

一个无法部署的微服务是没有用的,因此我们将添加一个 Dockerfile 来打包应用程序以便于分发—— 然后我保证让你走

FROM golang:1.15-alpine3.12 as builder
WORKDIR $GOPATH/src/github.com/johan-lejdung/go-microservice-api-guide
COPY ./ .
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -v
RUN cp go-microservice-api-guide /
FROM alpine:3.12
COPY --from=builder / /
CMD ["/go-microservice-api-guide"]
view raw Dockerfile hosted with ❤ by GitHub
FROM golang:1.15-alpine3.12 as builder
WORKDIR $GOPATH/src/github.com/johan-lejdung/go-microservice-api-guide
COPY ./ .
RUN GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -v
RUN cp go-microservice-api-guide /
FROM alpine:3.12
COPY --from=builder / /
CMD ["/go-microservice-api-guide"]
view raw Dockerfile hosted with ❤ by GitHub

构建的图像只有10MB!😱

代码如下。

REST API

克隆之前

在此处阅读随附文章: https: //medium.com/@johanlejdung/a-mini-guide-build-a-rest-api-as-a-go-microservice-together-with-mysql-fc203a6411c0

如果要将模块克隆到新位置,请更改模块名称。

go mod init <your_module_name>

如果您使用的是私有存储库,请确保使用以下命令设置 GOPRIVATE:

go env -w GOPRIVATE=github.com/repoURL/private-repo

设置

这里包含一个docker-compose文件,可用于在 docker 中启动 MySQL 数据库以运行此代码。

运行代码:

go run main.go

尝试插入一个值

curl -XPOST localhost:8080/endpoint -v

然后使用

curl localhost:8080/endpoint/1 -v

进一步阅读

如果您对最佳文件夹结构感兴趣,请查看此github.com/golang-standards/project-layout





希望你觉得这篇文章有趣,并且有所收获!当然,还有很多地方需要改进,但这正是你和你的创造力发挥作用的地方👍

如果您喜欢这篇文章,请与朋友分享或在 Twitter 上分享,我们将不胜感激!

我计划用更短的文章来涵盖更高级的主题😅。目前为止,我考虑过的主题包括:中间件的使用、测试、依赖注入和服务层。

文章来源:https://dev.to/johanlejdung/a-mini-guide-build-a-rest-api-as-a-go-microservice-together-with-mysql-27m2
PREV
良好 API 设计的实用建议
NEXT
为什么异步代码如此令人困惑(以及如何使其变得简单)强制同步的肮脏技巧 问题不在于异步代码 从一开始就异步编写 如果没有区别会怎样?异步函数组合 ESNext 提案:管道运算符 MojiScript MojiScript 异步示例 我需要你的帮助!总结