使用 Goa 在 Go 中开发 API
已发表: 2022-03-11API 开发是当今的热门话题。 您可以通过多种方式开发和交付 API,大公司已经开发了大量解决方案来帮助您快速启动应用程序。
然而,这些选项中的大多数都缺少一个关键特性:开发生命周期管理。 因此,开发人员花费了一些周期来创建有用且健壮的 API,但最终却为代码的预期有机演变以及 API 的微小变化对源代码的影响而苦苦挣扎。
2016 年,Raphael Simon 创建了 Goa,这是一个在 Golang 中用于 API 开发的框架,其生命周期将 API 设计放在首位。 在 Goa 中,您的 API 定义不仅被描述为代码,而且也是派生服务器代码、客户端代码和文档的来源。 这意味着您的代码在您的 API 定义中使用 Golang 领域特定语言 (DSL) 进行描述,然后使用 goa cli 生成,并与您的应用程序源代码分开实现。
这就是果阿大放异彩的原因。 它是一个具有明确定义的开发生命周期合同的解决方案,在生成代码时依赖于最佳实践(例如将不同的域和关注点分层,因此传输方面不会干扰应用程序的业务方面),遵循可组合的干净架构模式为应用程序中的传输层、端点层和业务逻辑层生成模块。
官方网站定义的一些 Goa 功能包括:
- 可组合性。 包、代码生成算法和生成的代码都是模块化的。
- 与传输无关。 传输层与实际服务实现的解耦意味着同一服务可以公开可通过多种传输(例如 HTTP 和/或 gRPC)访问的端点。
- 关注点分离。 实际的服务实现与传输代码隔离。
- Go 标准库类型的使用。 这使得与外部代码的交互更加容易。
在本文中,我将创建一个应用程序并引导您完成 API 开发生命周期的各个阶段。 该应用程序管理有关客户的详细信息,例如姓名、地址、电话号码、社交媒体等。最后,我们将尝试对其进行扩展并添加新功能以执行其开发生命周期。
那么,让我们开始吧!
准备您的开发区
我们的第一步是启动存储库并启用 Go 模块支持:
mkdir -p clients/design cd clients go mod init clients
最后,您的回购结构应如下所示:
$ tree . ├── design └── go.mod
设计你的 API
API 的真实来源是您的设计定义。 正如文档所述,“Goa 让您可以独立于任何实现问题考虑您的 API,然后在编写实现之前与所有利益相关者一起审查该设计。” 这意味着在生成实际应用程序代码之前,首先在此处定义 API 的每个元素。 但是说够了!
打开文件clients/design/design.go
并添加以下内容:
/* This is the design file. It contains the API specification, methods, inputs, and outputs using Goa DSL code. The objective is to use this as a single source of truth for the entire API source code. */ package design import ( . "goa.design/goa/v3/dsl" ) // Main API declaration var _ = API("clients", func() { Title("An api for clients") Description("This api manages clients with CRUD operations") Server("clients", func() { Host("localhost", func() { URI("http://localhost:8080/api/v1") }) }) }) // Client Service declaration with two methods and Swagger API specification file var _ = Service("client", func() { Description("The Client service allows access to client members") Method("add", func() { Payload(func() { Field(1, "ClientID", String, "Client ID") Field(2, "ClientName", String, "Client ID") Required("ClientID", "ClientName") }) Result(Empty) Error("not_found", NotFound, "Client not found") HTTP(func() { POST("/api/v1/client/{ClientID}") Response(StatusCreated) }) }) Method("get", func() { Payload(func() { Field(1, "ClientID", String, "Client ID") Required("ClientID") }) Result(ClientManagement) Error("not_found", NotFound, "Client not found") HTTP(func() { GET("/api/v1/client/{ClientID}") Response(StatusOK) }) }) Method("show", func() { Result(CollectionOf(ClientManagement)) HTTP(func() { GET("/api/v1/client") Response(StatusOK) }) }) Files("/openapi.json", "./gen/http/openapi.json") }) // ClientManagement is a custom ResultType used to configure views for our custom type var ClientManagement = ResultType("application/vnd.client", func() { Description("A ClientManagement type describes a Client of company.") Reference(Client) TypeName("ClientManagement") Attributes(func() { Attribute("ClientID", String, "ID is the unique id of the Client.", func() { Example("ABCDEF12356890") }) Field(2, "ClientName") }) View("default", func() { Attribute("ClientID") Attribute("ClientName") }) Required("ClientID") }) // Client is the custom type for clients in our database var Client = Type("Client", func() { Description("Client describes a customer of company.") Attribute("ClientID", String, "ID is the unique id of the Client Member.", func() { Example("ABCDEF12356890") }) Attribute("ClientName", String, "Name of the Client", func() { Example("John Doe Limited") }) Required("ClientID", "ClientName") }) // NotFound is a custom type where we add the queried field in the response var NotFound = Type("NotFound", func() { Description("NotFound is the type returned when " + "the requested data that does not exist.") Attribute("message", String, "Message of error", func() { Example("Client ABCDEF12356890 not found") }) Field(2, "id", String, "ID of missing data") Required("message", "id") })
首先你会注意到,上面的 DSL 是一组 Go 函数,可以组合来描述远程服务 API。 这些函数是使用匿名函数参数组成的。 在 DSL 函数中,我们有一个不应出现在其他函数中的函数子集,我们称之为顶级 DSL。 下面,您有部分 DSL 函数及其结构:
因此,在我们的初始设计中,我们有一个 API 顶级 DSL 描述我们的客户端 API,一个服务顶级 DSL 描述主要 API 服务、 clients
和服务 API swagger 文件,以及两种类型的顶级 DSL 用于描述传输负载中使用的对象视图类型。
API
函数是一个可选的顶级 DSL,它列出了 API 的全局属性,例如名称、描述,以及一个或多个可能公开不同服务集的服务器。 在我们的案例中,一台服务器就足够了,但您也可以在不同的层级提供不同的服务:例如,开发、测试和生产。
Service
函数定义了一组方法,这些方法可能映射到传输中的资源。 服务还可以定义常见的错误响应。 使用Method
描述服务方法。 此函数定义方法负载(输入)和结果(输出)类型。 如果省略有效负载或结果类型,则使用内置类型 Empty,它映射到 HTTP 中的空正文。
最后, Type
或ResultType
函数定义用户定义的类型,主要区别在于结果类型还定义了一组“视图”。
在我们的示例中,我们描述了 API 并解释了它应该如何服务,我们还创建了以下内容:
- 称为
clients
的服务 - 三种方法:
add
(用于创建一个客户端)、get
(用于检索一个客户端)和show
(用于列出所有客户端) - 我们自己的自定义类型,当我们与数据库集成时会派上用场,以及自定义的错误类型
现在已经描述了我们的应用程序,我们可以生成样板代码。 以下命令将设计包导入路径作为参数。 它还接受输出目录的路径作为可选标志:
goa gen clients/design
该命令输出它生成的文件的名称。 在那里, gen
目录包含应用程序名称子目录,其中包含与传输无关的服务代码。 http
子目录描述了 HTTP 传输(我们有服务器和客户端代码,其中包含对请求和响应进行编码和解码的逻辑,以及从命令行构建 HTTP 请求的 CLI 代码)。 它还包含 JSON 和 YAML 格式的 Open API 2.0 规范文件。
您可以复制 swagger 文件的内容并将其粘贴到任何在线 Swagger 编辑器(如 swagger.io 上的编辑器),以可视化您的 API 规范文档。 它们支持 YAML 和 JSON 格式。
现在,我们已为开发生命周期的下一步做好准备。
实现你的 API
创建样板代码后,是时候向其中添加一些业务逻辑了。 此时,您的代码应如下所示:
每当我们执行 CLI 时,Goa 都会维护和更新上面的每个文件。 因此,随着架构的发展,您的设计将跟随发展,您的源代码也将随之发展。 为了实现应用程序,我们执行以下命令(它将生成服务的基本实现以及可构建的服务器文件,这些文件启动 goroutine 以启动 HTTP 服务器和可以向该服务器发出请求的客户端文件):
goa example clients/design
这将生成一个包含服务器和客户端可构建源的 cmd 文件夹。 会有你的应用程序,这些是在 Goa 首次生成它们之后你应该自己维护的文件。
Goa 文档清楚地表明:“此命令为服务生成一个起点,以帮助引导开发 - 特别是它并不意味着在设计更改时重新运行。”
现在,您的代码将如下所示:
其中client.go
是一个示例文件,其中包含get
和show
方法的虚拟实现。 让我们为其添加一些业务逻辑!
为简单起见,我们将使用 SQLite 而不是内存数据库,并使用 Gorm 作为我们的 ORM。 创建文件sqlite.go
并添加以下内容 - 这将添加数据库逻辑以在数据库上创建记录并列出数据库中的一行和/或多行:
package clients import ( "clients/gen/client" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/sqlite" ) var db *gorm.DB var err error type Client *client.ClientManagement // InitDB is the function that starts a database file and table structures // if not created then returns db object for next functions func InitDB() *gorm.DB { // Opening file db, err := gorm.Open("sqlite3", "./data.db") // Display SQL queries db.LogMode(true) // Error if err != nil { panic(err) } // Creating the table if it doesn't exist var TableStruct = client.ClientManagement{} if !db.HasTable(TableStruct) { db.CreateTable(TableStruct) db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(TableStruct) } return db } // GetClient retrieves one client by its ID func GetClient(clientID string) (client.ClientManagement, error) { db := InitDB() defer db.Close() var clients client.ClientManagement db.Where("client_id = ?", clientID).First(&clients) return clients, err } // CreateClient created a client row in DB func CreateClient(client Client) error { db := InitDB() defer db.Close() err := db.Create(&client).Error return err } // ListClients retrieves the clients stored in Database func ListClients() (client.ClientManagementCollection, error) { db := InitDB() defer db.Close() var clients client.ClientManagementCollection err := db.Find(&clients).Error return clients, err }
然后,我们编辑 client.go 以更新客户端服务中的所有方法,实现数据库调用并构造 API 响应:
// Add implements add. func (s *clientsrvc) Add(ctx context.Context, p *client.AddPayload) (err error) { s.logger.Print("client.add started") newClient := client.ClientManagement{ ClientID: p.ClientID, ClientName: p.ClientName, } err = CreateClient(&newClient) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.add completed") return } // Get implements get. func (s *clientsrvc) Get(ctx context.Context, p *client.GetPayload) (res *client.ClientManagement, err error) { s.logger.Print("client.get started") result, err := GetClient(p.ClientID) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.get completed") return &result, err } // Show implements show. func (s *clientsrvc) Show(ctx context.Context) (res client.ClientManagementCollection, err error) { s.logger.Print("client.show started") res, err = ListClients() if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.show completed") return }
我们的应用程序的第一个剪辑已准备好编译。 运行以下命令以创建服务器和客户端二进制文件:
go build ./cmd/clients go build ./cmd/clients-cli
要运行服务器,只需运行./clients
。 暂时让它运行。 您应该会看到它成功运行,如下所示:
$ ./clients [clients] 00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Show" mounted on GET /api/v1/client [clients] 00:00:01 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [clients] 00:00:01 HTTP server listening on "localhost:8080"
我们准备在我们的应用程序中执行一些测试。 让我们尝试使用 cli 的所有方法:
$ ./clients-cli client add --body '{"ClientName": "Cool Company"}' \ --client-id "1" $ ./clients-cli client get --client-id "1" { "ClientID": "1", "ClientName": "Cool Company" } $ ./clients-cli client show [ { "ClientID": "1", "ClientName": "Cool Company" } ]
如果遇到任何错误,请检查服务器日志以确保 SQLite ORM 逻辑良好,并且您没有遇到任何数据库错误,例如数据库未初始化或查询未返回任何行。
扩展您的 API
该框架支持开发插件以扩展您的 API 并轻松添加更多功能。 Goa 有一个由社区创建的插件存储库。
正如我之前解释的那样,作为开发生命周期的一部分,我们可以依靠工具集来扩展我们的应用程序,方法是返回设计定义、更新它并刷新我们生成的代码。 让我们展示插件如何通过向 API 添加 CORS 和身份验证来提供帮助。
将文件clients/design/design.go
更新为以下内容:
/* This is the design file. It contains the API specification, methods, inputs, and outputs using Goa DSL code. The objective is to use this as a single source of truth for the entire API source code. */ package design import ( . "goa.design/goa/v3/dsl" cors "goa.design/plugins/v3/cors/dsl" ) // Main API declaration var _ = API("clients", func() { Title("An api for clients") Description("This api manages clients with CRUD operations") cors.Origin("/.*localhost.*/", func() { cors.Headers("X-Authorization", "X-Time", "X-Api-Version", "Content-Type", "Origin", "Authorization") cors.Methods("GET", "POST", "OPTIONS") cors.Expose("Content-Type", "Origin") cors.MaxAge(100) cors.Credentials() }) Server("clients", func() { Host("localhost", func() { URI("http://localhost:8080/api/v1") }) }) }) // Client Service declaration with two methods and Swagger API specification file var _ = Service("client", func() { Description("The Client service allows access to client members") Error("unauthorized", String, "Credentials are invalid") HTTP(func() { Response("unauthorized", StatusUnauthorized) }) Method("add", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Field(2, "ClientID", String, "Client ID") Field(3, "ClientName", String, "Client ID") Field(4, "ContactName", String, "Contact Name") Field(5, "ContactEmail", String, "Contact Email") Field(6, "ContactMobile", Int, "Contact Mobile Number") Required("token", "ClientID", "ClientName", "ContactName", "ContactEmail", "ContactMobile") }) Security(JWTAuth, func() { Scope("api:write") }) Result(Empty) Error("invalid-scopes", String, "Token scopes are invalid") Error("not_found", NotFound, "Client not found") HTTP(func() { POST("/api/v1/client/{ClientID}") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusCreated) }) }) Method("get", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Field(2, "ClientID", String, "Client ID") Required("token", "ClientID") }) Security(JWTAuth, func() { Scope("api:read") }) Result(ClientManagement) Error("invalid-scopes", String, "Token scopes are invalid") Error("not_found", NotFound, "Client not found") HTTP(func() { GET("/api/v1/client/{ClientID}") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusOK) }) }) Method("show", func() { Payload(func() { TokenField(1, "token", String, func() { Description("JWT used for authentication") }) Required("token") }) Security(JWTAuth, func() { Scope("api:read") }) Result(CollectionOf(ClientManagement)) Error("invalid-scopes", String, "Token scopes are invalid") HTTP(func() { GET("/api/v1/client") Header("token:X-Authorization") Response("invalid-scopes", StatusForbidden) Response(StatusOK) }) }) Files("/openapi.json", "./gen/http/openapi.json") }) // ClientManagement is a custom ResultType used to // configure views for our custom type var ClientManagement = ResultType("application/vnd.client", func() { Description("A ClientManagement type describes a Client of company.") Reference(Client) TypeName("ClientManagement") Attributes(func() { Attribute("ClientID", String, "ID is the unique id of the Client.", func() { Example("ABCDEF12356890") }) Field(2, "ClientName") Attribute("ContactName", String, "Name of the Contact.", func() { Example("John Doe") }) Field(4, "ContactEmail") Field(5, "ContactMobile") }) View("default", func() { Attribute("ClientID") Attribute("ClientName") Attribute("ContactName") Attribute("ContactEmail") Attribute("ContactMobile") }) Required("ClientID") }) // Client is the custom type for clients in our database var Client = Type("Client", func() { Description("Client describes a customer of company.") Attribute("ClientID", String, "ID is the unique id of the Client Member.", func() { Example("ABCDEF12356890") }) Attribute("ClientName", String, "Name of the Client", func() { Example("John Doe Limited") }) Attribute("ContactName", String, "Name of the Client Contact.", func() { Example("John Doe") }) Attribute("ContactEmail", String, "Email of the Client Contact", func() { Example("[email protected]") }) Attribute("ContactMobile", Int, "Mobile number of the Client Contact", func() { Example(12365474235) }) Required("ClientID", "ClientName", "ContactName", "ContactEmail", "ContactMobile") }) // NotFound is a custom type where we add the queried field in the response var NotFound = Type("NotFound", func() { Description("NotFound is the type returned " + "when the requested data that does not exist.") Attribute("message", String, "Message of error", func() { Example("Client ABCDEF12356890 not found") }) Field(2, "id", String, "ID of missing data") Required("message", "id") }) // Creds is a custom type for replying Tokens var Creds = Type("Creds", func() { Field(1, "jwt", String, "JWT token", func() { Example("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9" + "lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHD" + "cEfxjoYZgeFONFh7HgQ") }) Required("jwt") }) // JWTAuth is the JWTSecurity DSL function for adding JWT support in the API var JWTAuth = JWTSecurity("jwt", func() { Description(`Secures endpoint by requiring a valid JWT token retrieved via the signin endpoint. Supports scopes "api:read" and "api:write".`) Scope("api:read", "Read-only access") Scope("api:write", "Read and write access") }) // BasicAuth is the BasicAuth DSL function for // adding basic auth support in the API var BasicAuth = BasicAuthSecurity("basic", func() { Description("Basic authentication used to " + "authenticate security principal during signin") Scope("api:read", "Read-only access") }) // Signin Service is the service used to authenticate users and assign JWT tokens for their sessions var _ = Service("signin", func() { Description("The Signin service authenticates users and validate tokens") Error("unauthorized", String, "Credentials are invalid") HTTP(func() { Response("unauthorized", StatusUnauthorized) }) Method("authenticate", func() { Description("Creates a valid JWT") Security(BasicAuth) Payload(func() { Description("Credentials used to authenticate to retrieve JWT token") UsernameField(1, "username", String, "Username used to perform signin", func() { Example("user") }) PasswordField(2, "password", String, "Password used to perform signin", func() { Example("password") }) Required("username", "password") }) Result(Creds) HTTP(func() { POST("/signin/authenticate") Response(StatusOK) }) }) })
您可以注意到新设计的两个主要区别。 我们在client
服务中定义了一个安全范围,因此我们可以验证用户是否被授权调用该服务,并且我们定义了第二个名为signin
的服务,我们将使用它来验证用户并生成 JSON Web 令牌 (JWT), client
服务将用于授权调用。 我们还在自定义客户端类型中添加了更多字段。 这是开发 API 时的常见情况——需要重塑或重组数据。

在设计方面,这些更改可能听起来很简单,但反映它们,需要许多最小的功能来实现设计中描述的内容。 以使用我们的 API 方法进行身份验证和授权的架构示意图为例:
这些都是我们的代码还没有的新功能。 同样,这是 Goa 为您的开发工作增加更多价值的地方。 让我们通过使用以下命令再次生成源代码来在传输端实现这些功能:
goa gen clients/design
此时,如果您碰巧在使用 Git,您会注意到新文件的存在,而其他文件则显示为已更新。 这是因为 Goa 在没有我们干预的情况下相应地无缝刷新了样板代码。
现在,我们需要实现服务端代码。 在实际应用程序中,您将在更新源代码以反映所有设计更改后手动更新应用程序。 这是 Goa 建议我们继续进行的方式,但为简洁起见,我将删除并重新生成示例应用程序以使我们更快地到达那里。 运行以下命令以删除示例应用程序并重新生成它:
rm -rf cmd client.go goa example clients/design
这样,您的代码应如下所示:
我们可以在示例应用程序中看到一个新文件: signin.go
,其中包含登录服务逻辑。 但是,我们可以看到client.go
也更新了用于验证令牌的 JWTAuth 函数。 这与我们在设计中编写的内容相匹配,因此对客户端中任何方法的每次调用都将被拦截以进行令牌验证,并且只有在得到有效令牌和正确范围的授权时才会转发。
因此,我们将在signin.go
中更新我们的登录服务中的方法,以便添加逻辑来生成 API 将为经过身份验证的用户创建的令牌。 将以下上下文复制并粘贴到signin.go
:
package clients import ( signin "clients/gen/signin" "context" "log" "time" jwt "github.com/dgrijalva/jwt-go" "goa.design/goa/v3/security" ) // signin service example implementation. // The example methods log the requests and return zero values. type signinsrvc struct { logger *log.Logger } // NewSignin returns the signin service implementation. func NewSignin(logger *log.Logger) signin.Service { return &signinsrvc{logger} } // BasicAuth implements the authorization logic for service "signin" for the // "basic" security scheme. func (s *signinsrvc) BasicAuth(ctx context.Context, user, pass string, scheme *security.BasicScheme) (context.Context, error) { if user != "gopher" && pass != "academy" { return ctx, signin. Unauthorized("invalid username and password combination") } return ctx, nil } // Creates a valid JWT func (s *signinsrvc) Authenticate(ctx context.Context, p *signin.AuthenticatePayload) (res *signin.Creds, err error) { // create JWT token token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "nbf": time.Date(2015, 10, 10, 12, 0, 0, 0, time.UTC).Unix(), "iat": time.Now().Unix(), "exp": time.Now().Add(time.Duration(9) * time.Minute).Unix(), "scopes": []string{"api:read", "api:write"}, }) s.logger.Printf("user '%s' logged in", p.Username) // note that if "SignedString" returns an error then it is returned as // an internal error to the client t, err := token.SignedString(Key) if err != nil { return nil, err } res = &signin.Creds{ JWT: t, } return }
最后,因为我们在自定义类型中添加了更多字段,所以我们需要在client.go
中更新客户端 Service 的 Add 方法以反映这些更改。 复制并粘贴以下内容以更新您的client.go
:
package clients import ( client "clients/gen/client" "context" "log" jwt "github.com/dgrijalva/jwt-go" "goa.design/goa/v3/security" ) var ( // Key is the key used in JWT authentication Key = []byte("secret") ) // client service example implementation. // The example methods log the requests and return zero values. type clientsrvc struct { logger *log.Logger } // NewClient returns the client service implementation. func NewClient(logger *log.Logger) client.Service { return &clientsrvc{logger} } // JWTAuth implements the authorization logic for service "client" for the // "jwt" security scheme. func (s *clientsrvc) JWTAuth(ctx context.Context, token string, scheme *security.JWTScheme) (context.Context, error) { claims := make(jwt.MapClaims) // authorize request // 1. parse JWT token, token key is hardcoded to "secret" in this example _, err := jwt.ParseWithClaims(token, claims, func(_ *jwt.Token) (interface{}, error) { return Key, nil }) if err != nil { s.logger.Print("Unable to obtain claim from token, it's invalid") return ctx, client.Unauthorized("invalid token") } s.logger.Print("claims retrieved, validating against scope") s.logger.Print(claims) // 2. validate provided "scopes" claim if claims["scopes"] == nil { s.logger.Print("Unable to get scope since the scope is empty") return ctx, client.InvalidScopes("invalid scopes in token") } scopes, ok := claims["scopes"].([]interface{}) if !ok { s.logger.Print("An error occurred when retrieving the scopes") s.logger.Print(ok) return ctx, client.InvalidScopes("invalid scopes in token") } scopesInToken := make([]string, len(scopes)) for _, scp := range scopes { scopesInToken = append(scopesInToken, scp.(string)) } if err := scheme.Validate(scopesInToken); err != nil { s.logger.Print("Unable to parse token, check error below") return ctx, client.InvalidScopes(err.Error()) } return ctx, nil } // Add implements add. func (s *clientsrvc) Add(ctx context.Context, p *client.AddPayload) (err error) { s.logger.Print("client.add started") newClient := client.ClientManagement{ ClientID: p.ClientID, ClientName: p.ClientName, ContactName: p.ContactName, ContactEmail: p.ContactEmail, ContactMobile: p.ContactMobile, } err = CreateClient(&newClient) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.add completed") return } // Get implements get. func (s *clientsrvc) Get(ctx context.Context, p *client.GetPayload) (res *client.ClientManagement, err error) { s.logger.Print("client.get started") result, err := GetClient(p.ClientID) if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.get completed") return &result, err } // Show implements show. func (s *clientsrvc) Show(ctx context.Context, p *client.ShowPayload) (res client.ClientManagementCollection, err error) { s.logger.Print("client.show started") res, err = ListClients() if err != nil { s.logger.Print("An error occurred...") s.logger.Print(err) return } s.logger.Print("client.show completed") return }
就是这样! 让我们重新编译应用程序并再次测试它。 运行以下命令以删除旧的二进制文件并编译新的:
rm -f clients clients-cli go build ./cmd/clients go build ./cmd/clients-cli
再次运行./clients
并让它继续运行。 您应该会看到它成功运行,但这一次,实现了新方法:
$ ./clients [clients] 00:00:01 HTTP "Add" mounted on POST /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Get" mounted on GET /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "Show" mounted on GET /api/v1/client [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client/{ClientID} [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /api/v1/client [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /openapi.json [clients] 00:00:01 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [clients] 00:00:01 HTTP "Authenticate" mounted on POST /signin/authenticate [clients] 00:00:01 HTTP "CORS" mounted on OPTIONS /signin/authenticate [clients] 00:00:01 HTTP server listening on "localhost:8080"
为了测试,让我们使用 cli 执行所有 API 方法——注意我们使用的是硬编码的凭证:
$ ./clients-cli signin authenticate \ --username "gopher" --password "academy" { "JWT": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4 \ MSwibmJmIjoxNDQ0NDc4NDAwLCJzY29wZXMiOlsiY \ XBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" } $ ./clients-cli client add --body \ '{"ClientName": "Cool Company", \ "ContactName": "Jane Masters", \ "ContactEmail": "[email protected]", \ "ContactMobile": 13426547654 }' \ --client-id "1" --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" $ ./clients-cli client get --client-id "1" \ --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" { "ClientID": "1", "ClientName": "Cool Company", "ContactName": "Jane Masters", "ContactEmail": "[email protected]", "ContactMobile": 13426547654 } $ ./clients-cli client show \ --token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.\ eyJleHAiOjE1NzcyMTQxMjEsImlhdCI6MTU3NzIxMzU4MSwibmJmI\ joxNDQ0NDc4NDAwLCJzY29wZXMiOlsiYXBpOnJlYWQiLCJhcGk6d3JpdGUiXX0.\ tva_E3xbzur_W56pjzIll_pdFmnwmF083TKemSHQkSw" [ { "ClientID": "1", "ClientName": "Cool Company", "ContactName": "Jane Masters", "ContactEmail": "[email protected]", "ContactMobile": 13426547654 } ]
我们去吧! 我们有一个简约的应用程序,具有适当的身份验证、范围授权和进化增长空间。 在此之后,您可以使用云服务或您选择的任何其他身份提供者来开发自己的身份验证策略。 您还可以为您喜欢的数据库或消息传递系统创建插件,甚至可以轻松地与其他 API 集成。
查看 Goa 的 GitHub 项目以获取更多插件、示例(显示框架的特定功能)和其他有用的资源。
这就是今天的内容。 我希望你喜欢玩 Goa 并阅读这篇文章。 如果您对内容有任何反馈,请随时在 GitHub、Twitter 或 LinkedIn 上联系。
此外,我们在 Gophers Slack 的 #goa 频道上闲逛,所以过来打个招呼吧!
有关 Golang 的更多信息,请参阅 Go Programming Language: An Introductory Golang Tutorial 和 Well-structured Logic: A Golang OOP Tutorial。