在上一篇文章中,我解释了为REST API设置GO应用程序的基础。 现在,我将通过首先创建可配置服务器,添加http路由器(mux)和一些数据库交互来详细介绍。 让我们开始( 室内聚会)!
该应用程序现在在docker中运行,可以响应代码更改并重新加载以获得即时反馈。 为了处理http请求,我将添加另一个依赖项,即http路由器(mux)。 您可以在此处了解更多信息。
这是一款轻巧,高性能的HTTP请求路由器,易于使用,并具有大多数api所需的一切。
$ go get -u /julienschmidt/httprouter
是时候创建服务器了。 我将其放置在pkg /目录中,因为它可能被重用:
package server
import (
"errors"
"log"
"net/http"
"/julienschmidt/httprouter"
)
type Server struct {
srv *http.Server
}
func Get () * Server {
return &Server{
srv: &http.Server{},
}
}
func (s *Server) WithAddr (addr string ) * Server {
s.srv.Addr = addr
return s
}
func (s *Server) WithErrLogger (l *log.Logger) * Server {
s.srv.ErrorLog = l
return s
}
func (s *Server) WithRouter (router *httprouter.Router) * Server {
s.srv.Handler = router
return s
}
func (s *Server) Start () error {
if len (s.srv.Addr) == 0 {
return errors.New( "Server missing address" )
}
if s.srv.Handler == nil {
return errors.New( "Server missing handler" )
}
return s.srv.ListenAndServe()
}
func (s *Server) Close () error {
return s.srv.Close()
}
像往常一样, Get
函数返回指向我们服务器实例的指针,该实例公开了一些很容易解释的公共方法。 当我将此服务器放在主程序中时,它将变得更加明显。
服务器将需要路由和处理程序与外界进行通信。 接下来,我将其添加:
// cmd/api/router/router.go
package router
import (
"/boilerplate/cmd/api/handlers/getuser"
"/boilerplate/pkg/application"
"/julienschmidt/httprouter"
)
func Get (app *application.Application) * httprouter . Router {
mux := httprouter.New()
mux.GET( "/users" , (app))
return mux
}
// cmd/api/handlers/getuser/getuser.go
package getuser
import (
"fmt"
"net/http"
"/boilerplate/pkg/application"
"/julienschmidt/httprouter"
)
func Do (app *application.Application) httprouter . Handle {
return func (w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Fprintf(w, "hello" )
}
}
我在router.go
定义了所有路由,并通过显式传递应用程序配置来调用处理程序,以便每个处理程序都可以访问数据库,带有env vars的配置等内容。
我将处理程序与路由器分开,并将它们分组在子文件夹cmd/api/handlers/{handlerName}
。 原因之一是处理程序将具有相应的测试文件。 它还将具有多个中间件文件,这些中间件文件还将具有测试,并且可能会有很多处理程序。 如果没有正确分组,它可能会很快失控。
现在有更多的构建块:服务器,路由器,记录器。 让我们在主程序中组装它们:
// cmd/api/main.go
package main
import (
"/boilerplate/cmd/api/router"
"/boilerplate/pkg/application"
"/boilerplate/pkg/exithandler"
"/boilerplate/pkg/logger"
"/boilerplate/pkg/server"
"/joho/godotenv"
)
func main () {
if err := godotenv.Load(); err != nil {
.Println( "failed to load env vars" )
}
app, err := application.Get()
if err != nil {
logger.Error.Fatal(err.Error())
}
srv := server.
Get().
WithAddr(app.Cfg.GetAPIPort()).
WithRouter(router.Get(app)).
WithErrLogger(logger.Error)
go func () {
.Printf( "starting server at %s" , app.Cfg.GetAPIPort())
if err := srv.Start(); err != nil {
logger.Error.Fatal(err.Error())
}
}()
exithandler.Init( func () {
if err := srv.Close(); err != nil {
logger.Error.Println(err.Error())
}
if err := app.DB.Close(); err != nil {
logger.Error.Println(err.Error())
}
})
}
要提及的新事物是,我通过链接方法调用和在实例上设置属性来组装服务器。 一件有趣的事情是WithErrLogger(logger.Error)
,它只是指示服务器使用我的自定义记录器来保持一致性。
我在单独的go例程中启动服务器,以便exithandler
仍可以运行并正常处理程序关闭。 pkg/logger
包含2个标准库Logger的实例。 信息是打印出消息os.Stdout
和错误来os.Stderr
。 我本可以使用logrus
等花哨的记录器,但我打算使其保持简单。
接下来,让我们照顾数据库。 我使用用GO编写的迁移工具,可以用作CLI或库。 您可以阅读有关它的更多信息,并在此处找到安装说明。 安装后,让我们创建一些迁移文件。 从上面可以看出,我将在/users
资源上进行操作,因此很自然地会有users
表:
$ migrate create -ext sql -dir ./db/migrations create_user
这将产生2个迁移文件在db/migrations
, 向上和向下的用户表。 所有文件都是空的,因此我们添加一些sql。
上:
-- db/migrations/${timestamp}_create_user.up.sql
CREATE TABLE IF NOT EXISTS public.users
(
id SERIAL PRIMARY KEY ,
username VARCHAR ( 100 ) NOT NULL UNIQUE
);
向下:
-- db/migrations/${timestamp}_create_user.down.sql
DROP TABLE public.users
很简单,但是应该这样,对吧? 在运行迁移之前,让我们使用golang-migrate
库并创建一个程序来简化此过程。 这也将在CI / CD管道中很好地工作,因为它将使我们跳过golang-migrate
cli的安装, golang-migrate
是管道构建的单独步骤。 为此,我将添加另一个依赖项:
$ go get - u github. com /golang-migrate/migrate/v4
我将程序dbmigrate
:
// cmd/dbmigrate/main.go
package main
import (
"log"
"/boilerplate/pkg/config"
"/golang-migrate/migrate/v4"
_ "/golang-migrate/migrate/v4/database/postgres"
_ "/golang-migrate/migrate/v4/source/file"
"/joho/godotenv"
)
func main () {
godotenv.Load()
cfg := config.Get()
direction := cfg.GetMigration()
if direction != "down" && direction != "up" {
log.Fatal( "-migrate accepts [up, down] values only" )
}
m, err := migrate.New( "file://db/migrations" , cfg.GetDBConnStr())
if err != nil {
log.Fatal(err)
}
if direction == "up" {
if err := m.Up(); err != nil {
log.Fatal(err)
}
}
if direction == "down" {
if err := m.Down(); err != nil {
log.Fatal(err)
}
}
}
快速概览这里发生的事情。 首先,我加载env vars。 然后,我得到指向config实例的指针,这将使我可以通过一些辅助方法轻松访问所需的所有var。 您可能已经注意到,这里有一个新的GetMigration
方法。 它只会返回up或down字符串来指示我的程序是否应该向上或向下迁移数据库。 您可以在此处查看最新更改。
现在,由于我已经安装了此工具,因此可以使用它。 我发现它的最佳位置是scripts/entripoint.dev.sh
。在那里运行它可以避免常见的“哦,我忘了运行迁移”问题。 更新版本的entrypoint.dev.sh
:
#!/bin/bash
set -e
go run cmd/dbmigrate/main.go
go run cmd/dbmigrate/main.go -dbname=boilerplatetest
GO111MODULE=off go get /githubnemo/CompileDaemon
CompileDaemon --build= "go build -o main cmd/api/main.go" -- command =./main
这里发生了什么事? 第一次运行dbmigrate
将使用.env文件中的所有默认值,因此它将对boilerplate
数据库进行迁移。 在第二次运行中,我传递了-dbname=boilerplatetest
以便它执行相同的操作,但针对boilerplatetest
db。 接下来,我将以干净状态启动我的应用程序:
# remove all containers
docker container rm -f $(docker container ps -a -q)
# clear volumes
docker volume prune -f
# start app
docker-compose up --build
如果上面的所有工作过,我们应该看到users
在这两个表boilerplate
和boilerplatetest
数据库。 让我们检查一下:
# connect to pg docker container
docker exec -it $(docker ps --filter name=pg --format "{{.Names}}" ) /bin/bash
# launch psql cli
psql -U postgres -W
# ensure both DBs still present
\l
# connect to boilerplate database and list tables
\c boilerplate
\dt
# do same for boilerplatetest
\c boilerplatetest
\dt
# in both databases you should see users table
这是运行以上命令时看到的:
可以肯定的是,一切都如预期。 现在,如果我们在Docker中运行应用程序时添加新的迁移,该怎么办。 我敢肯定,停止docker-compose并再次重新运行命令以进行更改不是很方便。 好了, dbmigrate
程序能够处理这种情况。 在新的终端标签中:
# migrate boilerplatetest db down
go run cmd/dbmigrate/main.go \
-migrate=down \
-dbname=boilerplatetest \
-dbhost=localhost
# you can now repeat steps from above to connect to pg container
# and ensure that users table is missing from boilerplatetest DB.
# now bring it back up
go run cmd/dbmigrate/main.go \
-migrate=up \
-dbname=boilerplatetest \
-dbhost=localhost
这里要提到的一件事是-dbhost=localhost
。 这是因为我们从主机连接到pg容器。 在docker-compose中,我们可以通过服务名称pg
来引用相同的容器,但是我们不能从主机执行相同的操作。