如何创建Golang REST API:项目布局配置[第2部分]

在上一篇文章中,我解释了为REST API设置GO应用程序的基础。 现在,我将通过首先创建可配置服务器,添加http路由器(mux)和一些数据库交互来详细介绍。 让我们开始( 室内聚会)!

该应用程序现在在docker中运行,可以响应代码更改并重新加载以获得即时反馈。 为了处理http请求,我将添加另一个依赖项,即http路由器(mux)。 您可以在此处了解更多信息。

这是一款轻巧,高性能的HTTP请求路由器,易于使用,并具有大多数api所需的一切。

$ go get -u github.com/julienschmidt/httprouter

是时候创建服务器了。 我将其放置在pkg /目录中,因为它可能被重用:

package server

import (
	"errors"
	"log"
	"net/http"

	"github.com/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 (
	"github.com/boilerplate/cmd/api/handlers/getuser"
	"github.com/boilerplate/pkg/application"
	"github.com/julienschmidt/httprouter"
)

func Get (app *application.Application) * httprouter . Router {
	mux := httprouter.New()
	mux.GET( "/users" , getuser.Do(app))
	return mux
}

// cmd/api/handlers/getuser/getuser.go

package getuser

import (
	"fmt"
	"net/http"

	"github.com/boilerplate/pkg/application"
	"github.com/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 (
	"github.com/boilerplate/cmd/api/router"
	"github.com/boilerplate/pkg/application"
	"github.com/boilerplate/pkg/exithandler"
	"github.com/boilerplate/pkg/logger"
	"github.com/boilerplate/pkg/server"
	"github.com/joho/godotenv"
)

func main () {
	if err := godotenv.Load(); err != nil {
		logger.Info.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 () {
		logger.Info.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"

	"github.com/boilerplate/pkg/config"
	"github.com/golang-migrate/migrate/v4"
	_ "github.com/golang-migrate/migrate/v4/database/postgres"
	_ "github.com/golang-migrate/migrate/v4/source/file"
	"github.com/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方法。 它只会返回updown字符串来指示我的程序是否应该向上或向下迁移数据库。 您可以在此处查看最新更改。

现在,由于我已经安装了此工具,因此可以使用它。 我发现它的最佳位置是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 github.com/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在这两个表boilerplateboilerplatetest数据库。 让我们检查一下:

# 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来引用相同的容器,但是我们不能从主机执行相同的操作。

希望您学到了一些有用的东西。 在第3部分中,我将为用户资源进行简单的CRUD操作。 它将包括中间件的使用,验证等。 您还可以在此处查看整个项目并跟踪进度。 注意安全!

From: https://hackernoon.com/how-to-create-golang-rest-api-project-layout-configuration-part-2-wh2z3y5z

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值