golang底层深入_带有Golang的GraphQL:从基础到高级的深入研究

郑功
2023-12-01

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的库选项不多,但我已使用Thundergraphqlgraphql-gogqlgen对其进行了尝试 。 我不得不说, 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正式赞助了它们,因此我们将为这个很棒的开源项目看到更快的开发速度。 vektahneelance是主要贡献者, 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底层深入

 类似资料: