golang底層深入
by Ridham Tarpara
由里德姆·塔帕拉(Ridham Tarpara)
帶有Golang的GraphQL:從基礎到高級的深入研究 (GraphQL with Golang: A Deep Dive From Basics To Advanced)
GraphQL has become a buzzword over the last few years after Facebook made it open-source. I have tried GraphQL with the Node.js, and I agree with all the buzz about the advantages and simplicity of GraphQL.
在Facebook開源之后的最近幾年里,GraphQL已經成為流行語。 我已經使用Node.js嘗試了GraphQL,并且我對GraphQL的優點和簡單性一事都表示贊同。
So what is GraphQL? This is what the official GraphQL definition says:
那么,GraphQL是什么? 官方的GraphQL定義是這樣的:
GraphQL is a query language for APIs and runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
GraphQL是用于API和運行時的查詢語言,用于使用現有數據來完成這些查詢。 GraphQL為您的API中的數據提供完整且易于理解的描述,使客戶能夠準確地詢問他們所需的內容,僅此而已,使隨著時間的推移更易于開發API并啟用強大的開發人員工具。
I recently switched to Golang for a new project I’m working on (from Node.js) and I decided to try GraphQL with it. There are not many library options with Golang but I have tried it with Thunder, graphql, graphql-go, and gqlgen. And I have to say that gqlgen is winning among all the libraries I have tried.
我最近切換到Golang從事我正在研究的新項目(來自Node.js),我決定嘗試使用GraphQL。 Golang的庫選項不多,但我已使用Thunder , graphql , graphql-go和gqlgen對其進行了嘗試 。 我不得不說, gqlgen在我嘗試過的所有庫中勝出。
gqlgen is still in beta with latest version 0.7.2 at the time of writing this article, and it’s rapidly evolving. You can find their road-map here. And now 99designs is officially sponsoring them, so we will see even better development speed for this awesome open source project. vektah and neelance are major contributors, and neelance also wrote graphql-go.
在撰寫本文時, gqlgen仍處于beta版,最新版本為0.7.2 ,并且它正在Swift發展。 您可以在此處找到他們的路線圖。 現在99designs正式贊助了它們,因此我們將為這個很棒的開源項目看到更快的開發速度。 vektah和neelance是主要貢獻者, neelance還寫了graphql-go 。
So let’s dive into the library semantics assuming you have basic GraphQL knowledge.
因此,假設您具有基本的GraphQL知識,讓我們深入研究庫語義。
強調 (Highlights)
As their headline states,
作為他們的標題,
This is a library for quickly creating strictly typed GraphQL servers in Golang.
這是一個用于在Golang中快速創建嚴格類型的GraphQL服務器的庫。
I think this is the most promising thing about the library: you will never see map[string]interface{}
here, as it uses a strictly typed approach.
我認為這是庫中最有前途的事情:在這里您永遠不會看到map[string]interface{}
,因為它使用嚴格類型化的方法。
Apart from that, it uses a Schema first Approach: so you define your API using the graphql Schema Definition Language. This has its own powerful code generation tools which will auto-generate all of your GraphQL code and you will just need to implement the core logic of that interface method.
除此之外,它使用Schema first方法 :因此,您可以使用graphql Schema Definition Language定義API。 它具有自己強大的代碼生成工具,可以自動生成所有GraphQL代碼,您只需要實現該接口方法的核心邏輯即可。
I have divided this article into two phases:
我將本文分為兩個階段:
- The basics: Configuration, Mutations, Queries, and Subscription 基礎知識:配置,突變,查詢和訂閱
- The advanced: Authentication, Dataloaders, and Query Complexity 高級:身份驗證,數據加載器和查詢復雜性
階段1:基礎知識-配置,變異,查詢和訂閱 (Phase 1: The Basics - Configuration, Mutations, Queries, and Subscriptions)
We will use a video publishing site as an example in which a user can publish a video, add screenshots, add a review, and get videos and related videos.
我們將以視頻發布網站為例,用戶可以在其中發布視頻,添加屏幕截圖,添加評論以及獲取視頻和相關視頻。
mkdir -p $GOPATH/src/github.com/ridhamtarpara/go-graphql-demo/
Create the following schema in the project root:
在項目根目錄中創建以下架構:
Here we have defined our basic models and one mutation to publish new videos, and one query to get all videos. You can read more about the graphql schema here. We have also defined one custom type (scalar), as by default graphql has only 5 scalar types that include Int, Float, String, Boolean and ID.
在這里,我們定義了基本模型,定義了一種版本來發布新視頻,并定義了一種查詢來獲取所有視頻。 您可以在此處閱讀有關graphql 模式的更多信息。 我們還定義了一種自定義類型(標量),因為默認情況下graphql只有5種標量類型 ,包括Int,Float,String,Boolean和ID。
So if you want to use custom type, then you can define a custom scalar in schema.graphql
(like we have defined Timestamp
) and provide its definition in code. In gqlgen, you need to provide marshal and unmarshal methods for all custom scalars and map them to gqlgen.yml
.
因此,如果要使用自定義類型,則可以在schema.graphql
定義自定義標量(就像我們已經定義了Timestamp
),并在代碼中提供其定義。 在gqlgen中,您需要為所有自定義標量提供編組和解編方法,并將它們映射到gqlgen.yml
。
Another major change in gqlgen in the last version is that they have removed the dependency on compiled binaries. So add the following file to your project under scripts/gqlgen.go.
最新版本中gqlgen的另一個主要變化是,它們刪除了對已編譯二進制文件的依賴。 因此,將以下文件添加到腳本/gqlgen.go下的項目中。
and initialize dep with:
并使用以下命令初始化dep:
dep init
Now it’s time to take advantage of the library’s codegen feature which generates all the boring (but interesting for a few) skeleton code.
現在是時候利用該庫的codegen功能,該功能生成所有無聊的(但有一些有趣的)骨架代碼。
go run scripts/gqlgen.go init
which will create the following files:
這將創建以下文件:
gqlgen.yml — Config file to control code generation.generated.go — The generated code which you might not want to see.models_gen.go — All the models for input and type of your provided schema.resolver.go — You need to write your implementations.server/server.go — entry point with an http.Handler to start the GraphQL server.
gqlgen.yml —用于控制代碼生成的配置文件。 created.go —您可能不想看到的生成的代碼。 models_gen.go —用于提供的模式的輸入和類型的所有模型。 resolver.go-您需要編寫實現。 server / server.go —帶有http.Handler的入口點,用于啟動GraphQL服務器。
Let’s have a look at one of the generated models of the Video
type:
讓我們看一下Video
類型的生成模型之一:
Here, as you can see, ID is defined as a string and CreatedAt is also a string. Other related models are mapped accordingly, but in the real world you don’t want this — if you are using any SQL data type you want your ID field as int or int64, depending on your database.
如您所見,此處ID定義為字符串,CreatedAt也是字符串。 其他相關的模型也進行了相應的映射,但是在現實世界中,您不需要這樣做-如果您使用任何SQL數據類型,則希望ID字段為int或int64,具體取決于數據庫。
For example I am using PostgreSQL for demo so of course I want ID as an int and CreatedAt as a time.Time. So we need to define our own model and instruct gqlgen to use our model instead of generating a new one.
例如,我正在使用PostgreSQL進行演示,因此我當然希望ID為int且CreatedAt為time.Time 。 因此,我們需要定義自己的模型,并指示gqlgen使用我們的模型,而不是生成新模型。
and update gqlgen to use these models like this:
并更新gqlgen以使用以下模型:
So, the focal point is the custom definitions for ID and Timestamp with the marshal and unmarshal methods and their mapping in a gqlgen.yml file. Now when the user provides a string as ID, UnmarshalID will convert a string into an int. While sending the response, MarshalID will convert int to string. The same goes for Timestamp or any other custom scalar you define.
因此,重點是具有marshal和unmarshal方法的ID和Timestamp的自定義定義,以及它們在gqlgen.yml文件中的映射。 現在,當用戶提供字符串作為ID時,UnmarshalID會將字符串轉換為int。 發送響應時,MarshalID會將int轉換為字符串。 時間戳或您定義的任何其他自定義標量也是如此。
Now it’s time to implement real logic. Open resolver.go
and provide the definition to mutation and queries. The stubs are already auto-generated with a not implemented panic statement so let’s override that.
現在該實現真正的邏輯了。 打開resolver.go
并提供突變和查詢的定義。 存根已經通過未實現的panic語句自動生成,因此我們將其覆蓋。
and hit the mutation:
并擊中突變:
Ohh it worked….. but wait, why is my user empty ?? So here there is a similar concept like lazy and eager loading. As graphQL is extensible, you need to define which fields you want to populate eagerly and which ones lazily.
哦,行得通……..但是,為什么我的用戶為空? 因此,這里有類似的概念,例如延遲加載和渴望加載。 由于graphQL是可擴展的,因此您需要定義要急切填充的字段和懶散地填充的字段。
I have created this golden rule for my organization team working with gqlgen:
我為與gqlgen合作的組織團隊創建了這一黃金法則:
Don’t include the fields in a model which you want to load only when requested by the client.
不要僅在客戶要求時才在要加載的模型中包括字段。
For our use-case, I want to load Related Videos (and even users) only if a client asks for those fields. But as we have included those fields in the models, gqlgen will assume that you will provide those values while resolving video — so currently we are getting an empty struct.
對于我們的用例,僅當客戶要求這些字段時,我才想加載相關視頻(甚至用戶)。 但是,由于我們已將這些字段包括在模型中,因此gqlgen會假設您在解析視頻時會提供這些值-因此當前我們得到的是一個空結構。
Sometimes you need a certain type of data every time, so you don’t want to load it with another query. Rather you can use something like SQL joins to improve performance. For one use-case (not included in the article), I needed video metadata every time with the video which is stored in a different place. So if I loaded it when requested, I would need another query. But as I knew my requirements (that I need it everywhere on the client side), I preferred it to load eagerly to improve the performance.
有時您每次都需要某種類型的數據,因此您不想使用其他查詢來加載它。 而是可以使用諸如SQL連接之類的方法來提高性能。 對于一個用例(本文未包含),每次將視頻存儲在不同位置時,我都需要視頻元數據。 因此,如果我在請求時加載了它,則需要另一個查詢。 但是,由于我知道自己的要求(在客戶端的任何地方都需要它),所以我希望它能夠熱切地加載以提高性能。
So let’s rewrite the model and regenerate the gqlgen code. For the sake of simplicity, we will only define methods for the user.
因此,讓我們重寫模型并重新生成gqlgen代碼。 為了簡單起見,我們將只為用戶定義方法。
So we have added UserID and removed User struct and regenerated the code:
因此,我們添加了UserID并刪除了User結構并重新生成了代碼:
go run scripts/gqlgen.go -v
This will generate the following interface methods to resolve the undefined structs and you need to define those in your resolver:
這將生成以下接口方法來解析未定義的結構,您需要在解析器中定義它們:
And here is our definition:
這是我們的定義:
Now the result should look something like this:
現在結果應如下所示:
So this covers the very basics of graphql and should get you started. Try a few things with graphql and the power of Golang! But before that, let’s have a look at subscription which should be included in the scope of this article.
因此,這涵蓋了graphql的基礎知識,應該可以幫助您入門。 嘗試使用graphql和Golang的強大功能! 但是在此之前,讓我們看一下應該包含在本文范圍內的訂閱。
訂閱內容 (Subscriptions)
Graphql provides subscription as an operation type which allows you to subscribe to real tile data in GraphQL. gqlgen provides web socket-based real-time subscription events.
Graphql提供訂閱作為一種操作類型,使您可以訂閱GraphQL中的實際切片數據。 gqlgen提供基于Web套接字的實時訂閱事件。
You need to define your subscription in the schema.graphql
file. Here we are subscribing to the video publishing event.
您需要在schema.graphql
文件中定義您的訂閱。 在這里,我們正在訂閱視頻發布活動。
Regenerate the code by running: go run scripts/gqlgen.go -v
.
通過運行以下命令來重新生成代碼: go run scripts/gqlgen.go -v
。
As explained earlier, it will make one interface in generated.go which you need to implement in your resolver. In our case, it looks like this:
如前所述,它將在generate.go中創建一個接口,您需要在解析器中實現該接口。 在我們的例子中,它看起來像這樣:
Now, you need to emit events when a new video is created. As you can see on line 23 we have done that.
現在,您需要在創建新視頻時發出事件。 如您在第23行所看到的,我們已經做到了。
And it’s time to test the subscription:
現在可以測試訂閱了:
GraphQL comes with certain advantages, but everything that glitters is not gold. You need to take care of a few things like authorizations, query complexity, caching, N+1 query problem, rate limiting, and a few more issues — otherwise it will put you in performance jeopardy.
GraphQL具有某些優勢,但所有閃閃發光的東西都不是金子。 您需要注意一些事情,例如授權,查詢復雜性,緩存,N + 1查詢問題,速率限制以及其他一些問題,否則將使您陷入性能危機。
階段2:高級-身份驗證,數據加載器和查詢復雜性 (Phase 2: The advanced - Authentication, Dataloaders, and Query Complexity)
Every time I read a tutorial like this, I feel like I know everything I need to know and can get my all problems solved.
每次閱讀這樣的教程時,我都會感覺自己知道需要知道的一切,并且可以解決所有問題。
But when I start working on things on my own, I usually end up getting an internal server error or never-ending requests or dead ends and I have to dig deep into that to carve my way out. Hopefully we can help prevent that here.
但是,當我自己開始工作時,通常會遇到內部服務器錯誤或永無休止的請求或死胡同,而我必須深入研究該問題以找出出路。 希望我們可以在這里幫助防止這種情況。
Let’s take a look at a few advanced concepts starting with basic authentication.
讓我們看一些從基本身份驗證開始的高級概念。
認證方式 (Authentication)
In a REST API, you have a sort of authentication system and some out of the box authorizations on particular endpoints. But in GraphQL, only one endpoint is exposed so you can achieve this with schema directives.You need to edit your schema.graphql as follows:
在REST API中,您具有某種身份驗證系統,并且在特定端點上具有一些現成的授權。 但是在GraphQL中,僅公開了一個端點,因此您可以使用架構指令來實現此目的。您需要按如下方式編輯schema.graphql:
We have created an isAuthenticated directive and now we have applied that directive to createVideo
subscription. After you regenerate code you need to give a definition of the directive. Currently, directives are implemented as struct methods instead of the interface so we have to give a definition.I have updated the generated code of server.go and created a method to return graphql config for server.go as follows:
我們已經創建了一個isAuthenticated指令,現在已經將該指令應用于createVideo
訂閱。 重新生成代碼后,需要提供指令的定義。 當前,指令是作為struct方法而不是接口實現的,因此我們必須給出一個定義。我已經更新了server.go的生成代碼,并創建了一種方法來返回server.go的graphql config,如下所示:
We have read the userId from the context. Looks strange right? How was userId inserted in the context and why in context? Ok, so gqlgen only provides you the request contexts at the implementation level, so you can not read any of the HTTP request data like headers or cookies in graphql resolvers or directives. Therefore, you need to add your middleware and fetch those data and put the data in your context.
我們已經從上下文中讀取了userId。 看起來很奇怪吧? 如何將userId插入上下文中,為什么要插入上下文中? 好的,因此gqlgen僅在實現級別為您提供請求上下文,因此您無法讀取任何HTTP請求數據,例如graphql解析器或指令中的標頭或cookie。 因此,您需要添加中間件并獲取這些數據并將數據放入您的上下文中。
So we need to define auth middleware to fetch auth data from the request and validate.
因此,我們需要定義身份驗證中間件,以從請求中獲取身份驗證數據并進行驗證。
I haven’t defined any logic there, but instead I passed the userId as authorization for demo purposes. Then chain this middleware in server.go
along with the new config loading method.
我在那里沒有定義任何邏輯,但是我出于演示目的將userId作為授權傳遞。 然后將此中間件與新的配置加載方法鏈接到server.go
。
Now, the directive definition makes sense. Don’t handle unauthorized users in your middleware as it will be handled by your directive.
現在,指令定義變得有意義了。 不要處理中間件中未經授權的用戶,因為它將由您的指令處理。
Demo time:
演示時間:
You can even pass arguments in the schema directives like this:
您甚至可以在模式指令中傳遞參數,如下所示:
directive @hasRole(role: Role!) on FIELD_DEFINITIONenum Role { ADMIN USER }
數據加載器 (Dataloaders)
This all looks fancy, doesn’t it? You are loading data when needed. Clients have control of the data, there is no under-fetching and no over-fetching. But everything comes with a cost.
這一切看起來都不錯,不是嗎? 您將在需要時加載數據。 客戶端可以控制數據,不會出現數據提取不足和過度提取的情況。 但是,一切都是有代價的。
So what’s the cost here? Let’s take a look at the logs while fetching all the videos. We have 8 video entries and there are 5 users.
那這里的費用是多少? 在獲取所有視頻時,讓我們看一下日志。 我們有8個視頻條目,有5個用戶。
query{ Videos(limit: 10){ name user{ name } }}
Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1Resolver: User : SELECT id, name, email FROM users where id = $1
Why 9 queries (1 videos table and 8 users table)? It looks horrible. I was just about to have a heart attack when I thought about replacing our current REST API servers with this…but dataloaders came as a complete cure for it!
為什么要查詢9個(1個視頻表和8個用戶表)? 看起來太恐怖了。 當我想到用此替換當前的REST API服務器時,我正要心臟病發作……但是數據加載器可以完全治愈它!
This is known as the N+1 problem, There will be one query to get all the data and for each data (N) there will be another database query.
這稱為N + 1問題,將有一個查詢來獲取所有數據,對于每個數據(N),將有另一個數據庫查詢。
This is a very serious issue in terms of performance and resources: although these queries are parallel, they will use your resources up.
就性能和資源而言,這是一個非常嚴重的問題:盡管這些查詢是并行的,但它們會耗盡您的資源。
We will use the dataloaden library from the author of gqlgen. It is a Go- generated library. We will generate the dataloader for the user first.
我們將使用gqlgen的作者的dataloaden庫。 這是一個Go生成的庫。 我們將首先為用戶生成數據加載器。
go get github.com/vektah/dataloadendataloaden github.com/ridhamtarpara/go-graphql-demo/api.User
This will generate a file userloader_gen.go
which has methods like Fetch, LoadAll, and Prime.
這將生成一個文件userloader_gen.go
,該文件具有Fetch, userloader_gen.go
和Prime等方法。
Now, we need to define the Fetch method to get the result in bulk.
現在,我們需要定義Fetch方法來批量獲取結果。
Here, we are waiting for 1ms for a user to load queries and we have kept a maximum batch of 100 queries. So now, instead of firing a query for each user, dataloader will wait for either 1 millisecond for 100 users before hitting the database. We need to change our user resolver logic to use dataloader instead of the previous query logic.
在這里,我們等待用戶加載查詢的時間為1毫秒,并且我們最多保留了100個查詢。 因此,現在,數據加載器將不再為每個用戶觸發查詢,而是會在100個用戶之前等待1毫秒,然后再命中數據庫。 我們需要更改用戶解析器邏輯以使用數據加載器,而不是先前的查詢邏輯。
After this, my logs look like this for similar data:
在此之后,我的日志如下所示:
Query: Videos : SELECT id, name, description, url, created_at, user_id FROM videos ORDER BY created_at desc limit $1 offset $2Dataloader: User : SELECT id, name, email from users WHERE id IN ($1, $2, $3, $4, $5)
Now only two queries are fired, so everyone is happy. The interesting thing is that only five user keys are given to query even though 8 videos are there. So dataloader removed duplicate entries.
現在僅觸發兩個查詢,因此每個人都很高興。 有趣的是,即使有8個視頻,也只有五個用戶鍵可以查詢。 因此,數據加載器刪除了重復的條目。
查詢復雜度 (Query Complexity)
In GraphQL you are giving a powerful way for the client to fetch whatever they need, but this exposes you to the risk of denial of service attacks.
在GraphQL中,您為客戶端提供了一種獲取所需內容的強大方法,但這使您面臨拒絕服務攻擊的風險。
Let’s understand this through an example which we’ve been referring to for this whole article.
讓我們通過一個在整篇文章中一直引用的示例來理解這一點。
Now we have a related field in video type which returns related videos. And each related video is of the graphql video type so they all have related videos too…and this goes on.
現在,我們在視頻類型中有一個相關字段,該字段返回相關視頻。 每個相關視頻都是graphql視頻類型,因此它們也都具有相關視頻……而且這種情況還在繼續。
Consider the following query to understand the severity of the situation:
考慮以下查詢以了解情況的嚴重性:
{ Videos(limit: 10, offset: 0){ name url related(limit: 10, offset: 0){ name url related(limit: 10, offset: 0){ name url related(limit: 100, offset: 0){ name url } } } }}
If I add one more subobject or increase the limit to 100, then it will be millions of videos loading in one call. Perhaps (or rather definitely) this will make your database and service unresponsive.
如果我再添加一個子對象或將限制增加到100,則一次調用將加載數百萬個視頻。 也許(或者絕對是肯定的)這會使您的數據庫和服務無響應。
gqlgen provides a way to define the maximum query complexity allowed in one call. You just need to add one line (Line 5 in the following snippet) in your graphql handler and define the maximum complexity (300 in our case).
gqlgen提供了一種定義一次調用中允許的最大查詢復雜度的方法。 您只需要在graphql處理程序中添加一行(以下代碼段中的第5行),然后定義最大復雜度(本例中為300)。
gqlgen assigns fix complexity weight for each field so it will consider struct, array, and string all as equals. So for this query, complexity will be 12. But we know that nested fields weigh too much, so we need to tell gqlgen to calculate accordingly (in simple terms, use multiplication instead of just sum).
gqlgen為每個字段分配固定復雜度權重,因此它將結構,數組和字符串都視為相等。 因此,對于該查詢,復雜度將為12。但是我們知道嵌套字段的權重太大,因此我們需要告訴gqlgen進行相應的計算(簡單來說,請使用乘法而不是求和)。
Just like directives, complexity is also defined as struct, so we have changed our config method accordingly.
就像指令一樣,復雜度也定義為struct,因此我們相應地更改了config方法。
I haven’t defined the related method logic and just returned the empty array. So related is empty in the output, but this should give you a clear idea about how to use the query complexity.
我還沒有定義相關的方法邏輯,只是返回了空數組。 因此,輸出中的related是空的,但這應該使您對如何使用查詢復雜度有一個清晰的了解。
最后說明 (Final Notes)
This code is on Github. You can play around with it, and if you have any questions or concerns let me know in the comment section.
這段代碼在Github上 。 您可以嘗試一下,如果有任何疑問或疑慮,請在評論部分告訴我。
Thanks for reading! A few (hopefully 50) claps? are always appreciated. I write about JavaScript, the Go Language, DevOps, and Computer Science. Follow me and share this article if you like it.
謝謝閱讀! 拍手(希望有50個)? 總是很感激。 我寫有關JavaScript,Go語言,DevOps和計算機科學的文章。 關注我,如果您喜歡它,請分享這篇文章。
Reach out to me on @Twitter @Linkedin. Visit www.ridham.me for more.
通過@ Twitter @ Linkedin與我聯系。 有關更多信息,請訪問www.ridham.me 。
翻譯自: https://www.freecodecamp.org/news/deep-dive-into-graphql-with-golang-d3e02a429ac3/
golang底層深入