使用 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。