vlambda博客
学习文章列表

GitHub 很早就提供 GraphQL API 了,还不学习它就 Out 了

欢迎各位 Gophers !在本教程中,我们将探索如何使用 Go 和 GraphQL 服务进行交互。在本教程完结之时,我们希望你可以了解到以下内容:

  • GraphQL 的基础知识

  • 使用 Go 构建一个简易的 GraphQL 服务

  • 基于 GraphQL 执行一些基本的查询

在本篇教程中,我们会专注于 GraphQL 在数据检索方面的内容,我们将会使用内存数据源来存储其数据。同时,本篇教程的内容将会为我们之后的教程提供一个良好的基础。

GraphQL 的基础知识

在我们深入探讨之前,我们需要真正地理解 GraphQL 相关的基础知识。换句话说,作为开发人员,我们需要知道,使用它会为我们带来哪些益处。

考虑下,如果有一个系统,它每天需要处理数十万甚至数百万的请求。一般情况下,我们会请求一个面向数据库的 API ,该 API 会返回大量的 JSON 响应体,其中包含了许多冗余信息。

此时,如果我们面对的是一个超大规模的应用程序,那么发送、接收这些冗余信息会产生很多额外的开销。并且会由于负载的关系而阻塞带宽。

事实上,GraphQL 能够减少我们因冗余信息而产生的额外心智负担,并且,它拥有能够描述从服务端返回的数据的能力,这样,我们就可以仅关注当前任务,视图,或其他任何东西所需的数据、内容。

而且,上述的功能仅仅是 GraphQL 能提供给我们的众多益处中的很小的一部分。在接下来的教程中,我们将会介绍更多关于 GraphQL 的益处。

为 API 而生(而不是为数据库而生)的查询语言

一个非常重要的内容就是 GraphQL 不是一个和传统 SQL 一样的查询语言。它是位于 API 之前的一层抽象,且并不依赖于任何特定的数据或存储引擎。

这种设计非常酷,我们可以先建立一个与现有服务交互的 GraphQL 服务,然后围绕这个 GraphQL 进行构建,而无须担心会修改现有的 RESTful API。

REST 和 GraphQL 的区别

首先,让我们看看 RESTful 方法和 GraphQL 方法的区别。想象下我们正在构建一个能返回本站所有教程的服务,如果我们需要某些指定的教程信息,通常来说,我们会创建一个 API 端点以允许我们以一个 ID 来检索指定的教程。

# A dummy endpoint that takes in an ID path parameter
'http://api.tutorialedge.net/tutorial/:id'

如果给定的是一个合法的 ID ,它将会返回一个响应体,该响应体可能会如下所示:

{
"title": "Go GraphQL Tutorial",
"Author": "Elliot Forbes",
"slug": "/golang/go-graphql-beginners-tutorial/",
"views": 1,
"key" : "value"
}

现在,假设我们想创建一个控件,该控件会列出指定的作者撰写的前 5 个帖子。我们可以使用 /author/:id 这样的 API 来检索出所有由该作者撰写的帖子,然后再执行后续的调用获取排名前 5 的帖子。亦或者,我们可以创建一个新的 API 来返回这些数据。

上述的解决方案听起来并没有什么特别之处,因为它们创建了大量无用的请求或者返回了过多的冗余信息,这也暴露了 RESTful 方法的一些缺陷。

此时,就轮到 GraphQL 入场了。通过 GraphQL ,我们可以在查询中精确定义我们想要返回的数据。因此,如果我们需要上述的教程信息,我们可以创建一个查询,如下所示:

{
tutorial(id: 1) {
id
title
author {
name
tutorials
}
comments {
body
}
}
}

随后,它就会返回该教程的作者信息以及指定 id 教程下该作者所撰写的其他教程列表。这些数据都是我们所需的,但却不用通过发送额外的 REST 请求来获得!多美好,不是吗?

基本设置

目前为止,我们已经了解了 GraphQL 的基础知识以及使用它的益处,下面,让我们看看如何在实战中运用它。

我们将会使用 Go 创建一个简易的 GraphQL 服务,这里,我们是使用graphql-go/graphql[1] 这个库实现的。

设置简易的 GraphQL 服务

使用 go mod INIt 来初始化我们的项目:

$ Go mod INIt Github.com/elliotforbes/go-graphql-tutorial

接下来,让我们创建一个名为 main.go 的文件;并从头开始创建一个简易的 GraphQL 服务,它包含一个极简的解析器。

// credit - Go-graphql hello world example
package main

import (
"encoding/json"
"fmt"
"log"

"github.com/graphql-go/graphql"
)

func main() {
// Schema
fields := graphql.Fields{
"hello": &graphql.Field{
Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
return "world", nil
},
},
}
rootQuery := graphql.ObjectConfig{Name: "RootQuery", Fields: fields}
schemaConfig := graphql.SchemaConfig{Query: graphql.NewObject(rootQuery)}
schema, err := graphql.NewSchema(schemaConfig)
if err != nil {
log.Fatalf("failed to create new schema, error: %v", err)
}
// Query
query := `
{ hello
}
`

params := graphql.Params{Schema: schema, RequestString: query}
r := graphql.Do(params)
if len(r.Errors) > 0 {
log.Fatalf("failed to execute graphql operation, errors: %+v", r.Errors)
}
rJSON, _ := JSON.Marshal(r)
fmt.Printf("%s \n", rJSON) // { “ data ” :{ “ hello ” : ” world ” }}
}

现在,如果我们尝试运行它,看看会发生什么?

$ go run ./...
{"data":{"hello":"world"}}

所以,在一切正常的情况下,我们已经配置完成一个极简的 GraphQL 服务,并创建一个真实的请求发送至该服务。

GraphQL 模式

我们需要分解上述例子中的代码,以便于我们之后的扩展。在 fields... 代码行开始处,我们定义了一个 schema 。当我们通过 GraphQL API 执行查询时,我们实质上定义了对象中的哪些字段是我们期望得到的。所以我们必须在 schema 中定义这些字段。

Resolve... 代码行处,我们定义了一个解析器函数,每当这个特定 field 被请求时都会触发这个解析器函数。到目前为止,我们仅仅返回了一个 "world" 字符串,而在此之后,我们会实现查询整个数据库的能力。

查询

让我们继续分解 main.go 文件的剩下的部分。在 query... 代码行开始处,我们定义了一个请求 hello 字段的 query

接着,我们创建了一个 params 结构体,该结构体包含了对之前定义的 schema 以及 RequestString 请求的引用。

最后,我们开始执行请求,并将请求的结果填充到 r 中。然后我们处理可能出现的错误,并将响应体解析成 JSON ,并将其打印到终端上。

更为复杂的例子

目前为止,我们已经有一个运行中、极简的 GraphQL 服务,我们可以通过它来执行一些查询。我们需要更进一步,构建一个更为复杂的例子。

我们会创建一个 GraphQL 服务,它返回一系列存储于内存中的教程以及相应的作者、评论等信息。

首先,我们需要定义能够表示 TutorialAuthorComment 的结构体 :

type Tutorial struct {
Title string
Author Author
Comments []Comment
}

type Author struct {
Name string
Tutorials []int
}

type Comment struct {
Body string
}

紧接着,我们创建一个极简的 populate 函数用于返回元素为 Tutorial 类型的切片。

func populate() []Tutorial {
author := &Author{Name: "Elliot Forbes", Tutorials: []int{1}}
tutorial := Tutorial{
ID: 1,
Title: "Go GraphQL Tutorial",
Author: *author,
Comments: []Comment{
Comment{Body: "First Comment"},
},
}

var tutorials []Tutorial
tutorials = append(tutorials, tutorial)

return tutorials
}

上述的代码将为我们返回一个简单的教程列表,该列表会用于我们在之后进行的解析操作。

创建一个新的对象类型

首先,我们使用 graphql.NewObject() 在 GraphQL 中创建一个新对象。我们使用 GraphQL 严格的类型来定义三种不同的类型。这些类型将与我们已经定义好的三种结构体相匹配。

Comment 结构体无疑是最简单的,它只包含一个字符串类型的字段 Body ,所以我们能够很容易地将其表示为 commentType

var commentType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Comment",
// we define the name and the fields of our
// object. In this case, we have one solitary
// field that is of type string
Fields: graphql.Fields{
"body": &graphql.Field{
Type: graphql.String,
},
},
},
)

接着,我们需要处理 Author 结构体,并将其定义为新的 graphql.NewObject() ,这会稍微复杂一点,因为该结构体既包含 String 字段,也包含一个 Int 值列表,这些值表示该作者所编写的教程 ID 列表。

var authorType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Author",
Fields: graphql.Fields{
"Name": &graphql.Field{
Type: graphql.String,
},
"Tutorials": &graphql.Field{
// we'll use NewList to deal with an array
// of int values
Type: graphql.NewList(graphql.Int),
},
},
},
)

最后,我们定义了 tutorialType ,它会封装一个 author ,一个元素为 comment 的数组,ID 以及 title

var tutorialType = graphql.NewObject(
graphql.ObjectConfig{
Name: "Tutorial",
Fields: graphql.Fields{
"id": &graphql.Field{
Type: graphql.Int,
},
"title": &graphql.Field{
Type: graphql.String,
},
"author": &graphql.Field{
// here, we specify type as authorType
// which we've already defined.
// This is how we handle nested objects
Type: authorType,
},
"comments": &graphql.Field{
Type: graphql.NewList(commentType),
},
},
},
)

更新模式

到目前为止,我们已经定义了一个完整的 Type 系统,接下来,我们需要更新 Schema 以映射到这些类型上。我们会定义两个不同的 Field ,第一个是我们的 tutorial 字段,该字段允许我们根据传入的 ID 参数检索单个 tutorial 。第二个字段则是一个 list ,它允许我们检索存储于内存中的完整 tutorials 列表。

// Schema
fields := graphql.Fields{
"tutorial": &graphql.Field{
Type: tutorialType,
// it's Good form to add a description
// to each field.
Description: "Get Tutorial By ID",
// We can define arguments that allow us to
// pick specific tutorials. In this case
// we want to be able to specify the ID of the
// tutorial we want to retrieve
Args: graphql.FieldConfigArgument{
"id": &graphql.ArgumentConfig{
Type: graphql.Int,
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
// take in the ID argument
id, ok := p.Args["id"].(int)
if ok {
// Parse our tutorial array for the matching id
for _, tutorial := range tutorials {
if int(tutorial.ID) == id {
// return our tutorial
return tutorial, nil
}
}
}
return nil, nil
},
},
// this is our `list` endpoint which will return all
// tutorials available
"list": &graphql.Field{
Type: graphql.NewList(tutorialType),
Description: "Get Tutorial List",
Resolve: func(params graphql.ResolveParams) (interface{}, error) {
return tutorials, nil
},
},
}

到目前为止,我们创建了 Types 并更新了 GraphQL 模式,看起来我们似乎做的还算不错。

测试它是否能够工作

让我们先尝试新的 GraphQL 服务,使用我们最新提交的查询。通过更改 main 函数中的 query 来尝试 list 模式。

// Query
query := `
{
list {
id
title
comments {
body
}
author {
Name
Tutorials
}
}
}
`

我们需要分析一下,在此查询中有一个特殊的 root 对象。此时,我们描述了我们所期望的对象的 list 字段。在按照 list 模式返回的结果列表中,我们希望能够看到 idtitlecommentsauthor

当我们运行这个查询后,我们将会看到如下输出:

$ go run ./...
{"data":{"list":[{"author":{"Name":"Elliot Forbes","Tutorials":[1]},"comments":[{"body":"First Comment"}],"id":1,"title":"Go GraphQL Tutorial"}]}}

正如我们所看到的,我们的查询以 JSON 的格式返回了所有教程列表,这看起来和我们定义的初始查询非常相似。

现在让我们通过 tutorial 模式来执行另一个查询 :

query := `
{
tutorial(id:1) {
title
author {
Name
Tutorials
}
}
}
`

当我们再一次运行它,我们会看到它成功地检索到了内存中唯一一个 ID=1 的教程。

$ go run ./...
{"data":{"tutorial":{"author":{"Name":"Elliot Forbes","Tutorials":[1]},"title":"Go GraphQL Tutorial"}}}

完美,从输出结果上看,我们的 listtutorial 模式能够正常工作。

挑战:尝试在 populate 函数中更新教程列表,使其可以返回更多的教程。一旦我们完成了这一步,我们就可以尝试使用查询,并加深对查询的理解。

总结

注意:本教程全部的源代码位于这里:main.go[2]

这就是我们在本次初始教程中所介绍的所有内容。我们成功地配置了一个由内存数据存储支持的极简的 GraphQL 服务。

在下篇教程中,我们将查看 GraphQL 变更的概念,并改造我们的数据源以使用 NoSQL 数据库。关于下一篇教程可以阅读Go GraphQL Beginners Tutorial - Part2[3]


via: https://tutorialedge.net/golang/go-graphql-beginners-tutorial/

本文由 GCTT[7] 原创编译,Go 中文网[8] 荣誉推出

参考资料

[1]

graphql-go/graphql: https://github.com/graphql-go/graphql

[2]

main.go: https://gist.github.com/elliotforbes/9b8400ef5154eb3420e409aeffe39633

[3]

Go GraphQL Beginners Tutorial - Part2: https://tutorialedge.net/golang/go-graphql-beginners-tutorial-part-2/

[4]

Elliot Forbes: https://twitter.com/elliot_f

[5]

barryz: https://github.com/barryz

[6]

magichan: https://github.com/magichan

[7]

GCTT: https://github.com/studygolang/GCTT

[8]

Go 中文网: https://studygolang.com/