Skip to content

Latest commit

 

History

History
973 lines (752 loc) · 32.7 KB

File metadata and controls

973 lines (752 loc) · 32.7 KB

十二、反应式编程和数据流

在本章中,我们将讨论 Go 中的反应式编程设计模式。反应式编程是一种编程概念,侧重于数据流和变化的传播。卡夫卡等技术允许您快速生成或使用数据流。因此,这些技术自然适合彼此。在将卡夫卡连接到 Goflow配方中,我们将探索将kafka消息队列与goflow相结合,以展示使用这些技术的实际示例。本章还将探讨与卡夫卡联系的各种方式,并使用它来处理信息。最后,本章将演示如何在 Go 中创建一个基本的graphql服务器。

在本章中,我们将介绍以下配方:

  • 使用 Goflow 进行数据流编程
  • 用卡夫卡和萨拉玛
  • 与 Kafka 一起使用异步生产者
  • 将卡夫卡连接到 Goflow
  • 在 Go 中编写 GraphQL 服务器

技术要求

为了继续本章中的所有配方,请按照以下步骤配置您的环境:

  1. 下载 Go 1.12.6 或更高版本并安装到您的操作系统上 https://golang.org/doc/install.

  2. 打开终端或控制台应用,创建并导航到项目目录,如~/projects/go-programming-cookbook,所有代码都将从此目录运行和修改。

  3. 将最新代码克隆到~/projects/go-programming-cookbook-original中,并可以选择从该目录中工作,而不是手动键入示例:

$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original

使用 Goflow 进行数据流编程

github.com/trustmaster/goflow包对于创建基于数据流的应用非常有用。它试图抽象概念,以便您可以编写组件并使用自定义网络将它们连接在一起。此配方将重新创建第 9 章测试 Go 代码中讨论的应用,但将使用goflow包进行此操作。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter12/goflow的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/goflow 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/goflow   
  1. ~/projects/go-programming-cookbook-original/chapter12/goflow复制测试,或者将其作为练习来编写您自己的代码!
  2. 创建一个名为components.go的文件,其内容如下:
package goflow

import (
  "encoding/base64"
  "fmt"
)

// Encoder base64 encodes all input
type Encoder struct {
  Val <-chan string
  Res chan<- string
}

// Process does the encoding then pushes the result onto Res
func (e *Encoder) Process() {
  for val := range e.Val {
    encoded := base64.StdEncoding.EncodeToString([]byte(val))
    e.Res <- fmt.Sprintf("%s => %s", val, encoded)
  }
}

// Printer is a component for printing to stdout
type Printer struct {
  Line <-chan string
}

// Process Prints the current line received
func (p *Printer) Process() {
  for line := range p.Line {
    fmt.Println(line)
  }
}
  1. 创建一个名为network.go的文件,其内容如下:
package goflow

import (
  "github.com/trustmaster/goflow"
)

// NewEncodingApp wires together the components
func NewEncodingApp() *goflow.Graph {
  e := goflow.NewGraph()

  // define component types
  e.Add("encoder", new(Encoder))
  e.Add("printer", new(Printer))

  // connect the components using channels
  e.Connect("encoder", "Res", "printer", "Line")

  // map the in channel to Val, which is
  // tied to OnVal function
  e.MapInPort("In", "encoder", "Val")

  return e
}
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个名为main.go的文件,其内容如下:
package main

import (
  "fmt"

  "github.com/PacktPublishing/
   Go-Programming-Cookbook-Second-Edition/chapter12/goflow"
  flow "github.com/trustmaster/goflow"
)

func main() {

  net := goflow.NewEncodingApp()

  in := make(chan string)
  net.SetInPort("In", in)

  wait := flow.Run(net)

  for i := 0; i < 20; i++ {
    in <- fmt.Sprint("Message", i)
  }

  close(in)
  <-wait
}
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您现在应该看到以下输出:

$ go run main.go
Message6 => TWVzc2FnZTY=
Message5 => TWVzc2FnZTU=
Message1 => TWVzc2FnZTE=
Message0 => TWVzc2FnZTA=
Message4 => TWVzc2FnZTQ=
Message8 => TWVzc2FnZTg=
Message2 => TWVzc2FnZTI=
Message3 => TWVzc2FnZTM=
Message7 => TWVzc2FnZTc=
Message10 => TWVzc2FnZTEw
Message9 => TWVzc2FnZTk=
Message12 => TWVzc2FnZTEy
Message11 => TWVzc2FnZTEx
Message14 => TWVzc2FnZTE0
Message13 => TWVzc2FnZTEz
Message16 => TWVzc2FnZTE2
Message15 => TWVzc2FnZTE1
Message18 => TWVzc2FnZTE4
Message17 => TWVzc2FnZTE3
Message19 => TWVzc2FnZTE5
  1. go.mod文件可能会被更新,go.sum文件现在应该出现在顶级配方目录中。
  2. 如果您已经复制或编写了自己的测试,请转到一个目录并运行go test命令。确保所有测试都通过。

它是如何工作的。。。

github.com/trustmaster/goflow包的工作原理是定义一个网络/图形,注册一些组件,然后将它们连接在一起。由于组件是使用字符串描述的,这可能会让人感觉有点容易出错,但通常在运行时早期会失败,直到应用设置好并正常运行。

在这个配方中,我们设置了两个组件,一个是 Base64 编码传入字符串,另一个是打印传递给它的任何内容。我们将其连接到一个在main.go中初始化的 in 通道,任何传递到该通道的内容都将流经我们的管道。

这种方法的很多重点是忽略正在发生的事情的内部。我们把每件事都当作一个相连的黑匣子,剩下的就让goflow来做。您可以看到,在这个配方中,完成这个任务管道的代码是多么的小,控制工作人员数量的旋钮更少,等等。

用卡夫卡和萨拉玛

Kafka 是一种流行的分布式消息队列,具有许多用于构建分布式系统的高级功能。这个菜谱将展示如何使用同步生产者写入卡夫卡主题,以及如何使用分区消费者使用同一主题。本食谱不会探讨卡夫卡的不同配置,因为这是一个超出本书范围的更广泛的主题,但我建议从开始 https://kafka.apache.org/intro

准备

根据以下步骤配置您的环境:

  1. 参见本章开头的技术要求章节。
  2. 按照中提到的步骤安装卡夫卡 https://www.tutorialspoint.com/apache_kafka/apache_kafka_installation_steps.htm
  3. 或者,您也可以访问https://github.com/spotify/docker-kafka

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter12/synckafka的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/synckafka 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/synckafka   
  1. ~/projects/go-programming-cookbook-original/chapter12/synckafka复制测试,或者将其作为练习来编写您自己的代码!

  2. 确保卡夫卡在localhost:9092上启动并运行。

  3. 在名为consumer的目录中创建一个名为main.go的文件,其内容如下:

        package main

        import (
            "log"

            sarama "github.com/Shopify/sarama"
        )

        func main() {
            consumer, err := 
            sarama.NewConsumer([]string{"localhost:9092"}, nil)
            if err != nil {
                panic(err)
            }
            defer consumer.Close()

            partitionConsumer, err := 

           consumer.ConsumePartition("example", 0, 
            sarama.OffsetNewest)
            if err != nil {
                panic(err)
            }
            defer partitionConsumer.Close()

            for {
                msg := <-partitionConsumer.Messages()
                log.Printf("Consumed message: \"%s\" at offset: %d\n", 
                msg.Value, msg.Offset)
            }
        }
  1. 在名为producer的目录中创建一个名为main.go的文件,其内容如下:
        package main

        import (

           "fmt"
           "log"

            sarama "github.com/Shopify/sarama"
        )

        func sendMessage(producer sarama.SyncProducer, value string) {
            msg := &sarama.ProducerMessage{Topic: "example", Value: 
            sarama.StringEncoder(value)}
            partition, offset, err := producer.SendMessage(msg)
            if err != nil {

               log.Printf("FAILED to send message: %s\n", err)
                return
            }
            log.Printf("> message sent to partition %d at offset %d\n", 
            partition, offset)
        }

        func main() {
            producer, err := 
            sarama.NewSyncProducer([]string{"localhost:9092"}, nil)
            if err != nil {
                panic(err)
            }
            defer producer.Close()

            for i := 0; i < 10; i++ {
                sendMessage(producer, fmt.Sprintf("Message %d", i))
            }
        }
  1. 向上浏览目录。
  2. 运行go run ./consumer
  3. 在同一目录的单独终端中,运行go run ./producer
  4. 在 producer 终端中,您应该看到以下内容:
$ go run ./producer 
2017/05/07 11:50:38 > message sent to partition 0 at offset 0
2017/05/07 11:50:38 > message sent to partition 0 at offset 1
2017/05/07 11:50:38 > message sent to partition 0 at offset 2
2017/05/07 11:50:38 > message sent to partition 0 at offset 3
2017/05/07 11:50:38 > message sent to partition 0 at offset 4
2017/05/07 11:50:38 > message sent to partition 0 at offset 5
2017/05/07 11:50:38 > message sent to partition 0 at offset 6
2017/05/07 11:50:38 > message sent to partition 0 at offset 7
2017/05/07 11:50:38 > message sent to partition 0 at offset 8
2017/05/07 11:50:38 > message sent to partition 0 at offset 9

在消费者终端中,您应该看到:

$ go run ./consumer
2017/05/07 11:50:38 Consumed message: "Message 0" at offset: 0
2017/05/07 11:50:38 Consumed message: "Message 1" at offset: 1
2017/05/07 11:50:38 Consumed message: "Message 2" at offset: 2
2017/05/07 11:50:38 Consumed message: "Message 3" at offset: 3
2017/05/07 11:50:38 Consumed message: "Message 4" at offset: 4
2017/05/07 11:50:38 Consumed message: "Message 5" at offset: 5
2017/05/07 11:50:38 Consumed message: "Message 6" at offset: 6
2017/05/07 11:50:38 Consumed message: "Message 7" at offset: 7
2017/05/07 11:50:38 Consumed message: "Message 8" at offset: 8
2017/05/07 11:50:38 Consumed message: "Message 9" at offset: 9
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试通过。

它是如何工作的。。。

这个食谱演示了通过卡夫卡传递简单的消息。更复杂的方法应该使用序列化格式,如jsongobprotobuf或其他格式。制作人可以通过sendMessage同步向卡夫卡发送消息。这不能很好地处理卡夫卡集群关闭的情况,并可能导致这些情况的挂起过程。对于 Web 处理器这样的应用来说,这是很重要的,因为这可能会导致卡夫卡集群的超时和硬依赖。

假设消息队列正确,我们的消费者将观察 Kafka 流并对结果进行处理。本章前面的配方可能会利用此流进行一些额外的处理。

与 Kafka 一起使用异步生产者

在开始下一个任务之前,不要等待卡夫卡制作人完成,这通常很有用。在这种情况下,您可以使用异步生产者。这些生产者在一个通道上接收 Sarama 消息,并具有返回成功/错误通道的方法,该通道可以单独检查。

在此配方中,我们将创建一个 Go 例程,该例程将处理成功和失败消息,同时允许处理程序对要发送的消息进行排队,而不管结果如何。

准备

参考使用卡夫卡与萨拉玛配方中的准备部分。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter12/asynckafka的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/asynckafka 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/asynckafka   
  1. ~/projects/go-programming-cookbook-original/chapter12/asynckafka复制测试,或者将其作为练习来编写您自己的代码!
  2. 确保卡夫卡在localhost:9092上启动并运行。
  3. 从上一个配方复制消费者目录。
  4. 创建一个名为producer的目录并导航到它。
  5. 创建一个名为producer.go的文件,其内容如下:
        package main

        import (
            "log"

            sarama "github.com/Shopify/sarama"
        )

        // Process response grabs results and errors from a producer
        // asynchronously
        func ProcessResponse(producer sarama.AsyncProducer) {
            for {
                select {
                    case result := <-producer.Successes():
                    log.Printf("> message: \"%s\" sent to partition 
                    %d at offset %d\n", result.Value, 
                    result.Partition, result.Offset)
                    case err := <-producer.Errors():
                    log.Println("Failed to produce message", err)
                }
            }
        }
  1. 创建一个名为handler.go的文件,其内容如下:
        package main

        import (
            "net/http"

            sarama "github.com/Shopify/sarama"
        )

        // KafkaController allows us to attach a producer
        // to our handlers
        type KafkaController struct {
            producer sarama.AsyncProducer
        }

        // Handler grabs a message from a GET parama and
        // send it to the kafka queue asynchronously
        func (c *KafkaController) Handler(w http.ResponseWriter, r 
        *http.Request) {
            if err := r.ParseForm(); err != nil {
                w.WriteHeader(http.StatusBadRequest)
                return
            }

            msg := r.FormValue("msg")
            if msg == "" {
                w.WriteHeader(http.StatusBadRequest)
                w.Write([]byte("msg must be set"))
                return
            }
            c.producer.Input() <- &sarama.ProducerMessage{Topic: 
            "example", Key: nil, Value: 
            sarama.StringEncoder(msg)}
            w.WriteHeader(http.StatusOK)
        }
  1. 创建一个名为main.go的文件,其内容如下:
        package main

        import (
            "fmt"
            "net/http"

            sarama "github.com/Shopify/sarama"
        )

        func main() {
            config := sarama.NewConfig()
            config.Producer.Return.Successes = true
            config.Producer.Return.Errors = true
            producer, err := 
            sarama.NewAsyncProducer([]string{"localhost:9092"}, config)
            if err != nil {
                panic(err)
            }
            defer producer.AsyncClose()

            go ProcessResponse(producer)

            c := KafkaController{producer}
            http.HandleFunc("/", c.Handler)
            fmt.Println("Listening on port :3333")
            panic(http.ListenAndServe(":3333", nil))
        }
  1. 向上浏览目录。
  2. 运行go run ./consumer
  3. 在同一目录的单独终端中,运行go run ./producer
  4. 在第三个终端中,运行以下命令:
$ curl "http://localhost:3333/?msg=this"      
$ curl "http://localhost:3333/?msg=is" 
$ curl "http://localhost:3333/?msg=an" 
$ curl "http://localhost:3333/?msg=example" 

在 producer 终端中,您应该看到以下内容:

$ go run ./producer
Listening on port :3333
2017/05/07 13:52:54 > message: "this" sent to partition 0 at offset 0
2017/05/07 13:53:25 > message: "is" sent to partition 0 at offset 1
2017/05/07 13:53:27 > message: "an" sent to partition 0 at offset 2
2017/05/07 13:53:29 > message: "example" sent to partition 0 at offset 3
  1. 在消费者终端中,您应该看到:
$ go run ./consumer
2017/05/07 13:52:54 Consumed message: "this" at offset: 0
2017/05/07 13:53:25 Consumed message: "is" at offset: 1
2017/05/07 13:53:27 Consumed message: "an" at offset: 2
2017/05/07 13:53:29 Consumed message: "example" at offset: 3
  1. go.mod文件可能会被更新,go.sum文件现在应该出现在顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

我们在本章中的修改都是对制作人进行的。这一次,我们创建了一个单独的 Go 例程来处理成功和错误。如果这些未经处理,应用将死锁。接下来,我们将生产者连接到处理程序,并通过对处理程序的GET调用,在收到消息时在其上发出消息。

处理程序将在发送消息时立即返回 success,而不管其响应如何。如果这是不可接受的,则应使用同步方法。在我们的例子中,我们可以稍后分别处理成功和错误。

最后,我们curl使用一些不同的消息来显示端点,您可以看到它们从处理程序流向最终由卡夫卡消费者打印的地方,我们在上一节中写道。

将卡夫卡连接到 Goflow

此配方将结合卡夫卡消费者和 Goflow 管道。当我们的消费者收到来自卡夫卡的消息时,它将在这些消息上运行strings.ToUpper(),然后打印结果。这些自然配对,因为 Goflow 被设计为对传入流进行操作,这正是卡夫卡提供给我们的。

准备

参考使用卡夫卡与萨拉玛配方中的准备部分。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter12/kafkaflow的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/kafkaflow 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/kafkaflow   
  1. ~/projects/go-programming-cookbook-original/chapter12/kafkaflow复制测试,或者将其作为练习来编写您自己的代码!
  2. 确保卡夫卡在localhost:9092上启动并运行。
  3. 创建一个名为components.go的文件,其内容如下:
package kafkaflow

import (
  "fmt"
  "strings"

  flow "github.com/trustmaster/goflow"
)

// Upper upper cases the incoming
// stream
type Upper struct {
  Val <-chan string
  Res chan<- string
}

// Process loops over the input values and writes the upper
// case string version of them to Res
func (e *Upper) Process() {
  for val := range e.Val {
    e.Res <- strings.ToUpper(val)
  }
}

// Printer is a component for printing to stdout
type Printer struct {
  flow.Component
  Line <-chan string
}

// Process Prints the current line received
func (p *Printer) Process() {
  for line := range p.Line {
    fmt.Println(line)
  }
}
  1. 创建一个名为network.go的文件,其内容如下:
package kafkaflow

import "github.com/trustmaster/goflow"

// NewUpperApp wires together the components
func NewUpperApp() *goflow.Graph {
  u := goflow.NewGraph()

  u.Add("upper", new(Upper))
  u.Add("printer", new(Printer))

  u.Connect("upper", "Res", "printer", "Line")
  u.MapInPort("In", "upper", "Val")

  return u
}
  1. 在名为consumer的目录中创建一个名为main.go的文件,其内容如下:
package main

import (
  "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/kafkaflow"
  sarama "github.com/Shopify/sarama"
  flow "github.com/trustmaster/goflow"
)

func main() {
  consumer, err := sarama.NewConsumer([]string{"localhost:9092"}, nil)
  if err != nil {
    panic(err)
  }
  defer consumer.Close()

  partitionConsumer, err := consumer.ConsumePartition("example", 0, sarama.OffsetNewest)
  if err != nil {
    panic(err)
  }
  defer partitionConsumer.Close()

  net := kafkaflow.NewUpperApp()

  in := make(chan string)
  net.SetInPort("In", in)

  wait := flow.Run(net)
  defer func() {
    close(in)
    <-wait
  }()

  for {
    msg := <-partitionConsumer.Messages()
    in <- string(msg.Value)
  }

}
  1. 使用 Kafka with Saram 配方从复制producer目录。
  2. 运行go run ./consumer
  3. 在同一目录的单独终端中,运行go run ./producer
  4. 在 producer 终端中,您现在应该看到以下内容:
$ go run ./producer 
2017/05/07 18:24:12 > message "Message 0" sent to partition 0 at offset 0
2017/05/07 18:24:12 > message "Message 1" sent to partition 0 at offset 1
2017/05/07 18:24:12 > message "Message 2" sent to partition 0 at offset 2
2017/05/07 18:24:12 > message "Message 3" sent to partition 0 at offset 3
2017/05/07 18:24:12 > message "Message 4" sent to partition 0 at offset 4
2017/05/07 18:24:12 > message "Message 5" sent to partition 0 at offset 5
2017/05/07 18:24:12 > message "Message 6" sent to partition 0 at offset 6
2017/05/07 18:24:12 > message "Message 7" sent to partition 0 at offset 7
2017/05/07 18:24:12 > message "Message 8" sent to partition 0 at offset 8
2017/05/07 18:24:12 > message "Message 9" sent to partition 0 at offset 9

在消费终端中,您应该看到以下内容:

$ go run ./consumer
MESSAGE 0
MESSAGE 1
MESSAGE 2
MESSAGE 3
MESSAGE 4
MESSAGE 5
MESSAGE 6
MESSAGE 7
MESSAGE 8
MESSAGE 9
  1. go.mod文件可能会被更新,go.sum文件现在应该出现在顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

本食谱结合了本章先前食谱中的观点。与之前的食谱一样,我们设立了卡夫卡消费者和制作人。此配方使用中使用 Kafka 和 Sarama配方的同步生成器,但也可以使用异步生成器。一旦收到一条消息,我们将其放入一个 in 通道,就像我们在Goflow for dataflow programming配方*中所做的一样。*我们修改此配方中的组件,将传入字符串转换为大写,而不是对其进行 Base64 编码。我们重用打印组件,得到的网络配置类似。

最终结果是,通过 Kafka 消费者接收的所有消息都被传输到我们基于流的工作管道中进行操作。这使我们能够对管道组件进行模块化和可重用的测试,并且我们可以在不同的配置中多次使用相同的组件。类似地,我们将接收来自任何写入 Kafka 的生产者的流量,因此我们可以将生产者多路复用到单个数据流中。

在 Go 中编写 GraphQL 服务器

GraphQL 是 REST 的替代品,由 Facebook(创建 http://graphql.org/ )。这项技术允许服务器实现和发布模式,然后客户端可以请求他们需要的信息,而不是理解和使用各种 API 端点。

对于这个配方,我们将创建一个表示一副扑克牌的Graphql模式。我们将公开一张资源卡,它可以通过套装和价值进行过滤。或者,如果未指定参数,此模式可以返回组中的所有卡片。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter12/graphql的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/graphql 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter12/graphql   
  1. ~/projects/go-programming-cookbook-original/chapter12/graphql复制测试,或者将其作为练习来编写您自己的代码!

  2. 创建并导航到cards目录。

  3. 创建一个名为card.go的文件,其内容如下:

        package cards

        // Card represents a standard playing
        // card
        type Card struct {
            Value string
            Suit string
        }

        var cards []Card

        func init() {
            cards = []Card{
                {"A", "Spades"}, {"2", "Spades"}, {"3", "Spades"},
                {"4", "Spades"}, {"5", "Spades"}, {"6", "Spades"},
                {"7", "Spades"}, {"8", "Spades"}, {"9", "Spades"},
                {"10", "Spades"}, {"J", "Spades"}, {"Q", "Spades"},
                {"K", "Spades"},
                {"A", "Hearts"}, {"2", "Hearts"}, {"3", "Hearts"},
                {"4", "Hearts"}, {"5", "Hearts"}, {"6", "Hearts"},
                {"7", "Hearts"}, {"8", "Hearts"}, {"9", "Hearts"},
                {"10", "Hearts"}, {"J", "Hearts"}, {"Q", "Hearts"},
                {"K", "Hearts"},
                {"A", "Clubs"}, {"2", "Clubs"}, {"3", "Clubs"},
                {"4", "Clubs"}, {"5", "Clubs"}, {"6", "Clubs"},
                {"7", "Clubs"}, {"8", "Clubs"}, {"9", "Clubs"},
                {"10", "Clubs"}, {"J", "Clubs"}, {"Q", "Clubs"},
                {"K", "Clubs"},
                {"A", "Diamonds"}, {"2", "Diamonds"}, {"3", 
                "Diamonds"},
                {"4", "Diamonds"}, {"5", "Diamonds"}, {"6", 
                "Diamonds"},
                {"7", "Diamonds"}, {"8", "Diamonds"}, {"9", 
                "Diamonds"},
                {"10", "Diamonds"}, {"J", "Diamonds"}, {"Q", 
                "Diamonds"},
                {"K", "Diamonds"},
            }
        }
  1. 创建一个名为type.go的文件,其内容如下:
        package cards

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

        // CardType returns our card graphql object
        func CardType() *graphql.Object {
            cardType := graphql.NewObject(graphql.ObjectConfig{
                Name: "Card",
                Description: "A Playing Card",
                Fields: graphql.Fields{
                    "value": &graphql.Field{
                        Type: graphql.String,
                        Description: "Ace through King",
                        Resolve: func(p graphql.ResolveParams) 
                        (interface{}, error) {
                            if card, ok := p.Source.(Card); ok {
                                return card.Value, nil
                            }
                            return nil, nil
                        },
                    },
                    "suit": &graphql.Field{
                        Type: graphql.String,
                        Description: "Hearts, Diamonds, Clubs, Spades",
                        Resolve: func(p graphql.ResolveParams) 
                        (interface{}, error) {
                            if card, ok := p.Source.(Card); ok {
                                return card.Suit, nil
                            }
                            return nil, nil
                        },
                    },
                },
            })
            return cardType
        }
  1. 创建一个名为resolve.go的文件,其内容如下:
        package cards

        import (
            "strings"

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

        // Resolve handles filtering cards
        // by suit and value
        func Resolve(p graphql.ResolveParams) (interface{}, error) {
            finalCards := []Card{}
            suit, suitOK := p.Args["suit"].(string)
            suit = strings.ToLower(suit)

            value, valueOK := p.Args["value"].(string)
            value = strings.ToLower(value)

            for _, card := range cards {
                if suitOK && suit != strings.ToLower(card.Suit) {
                    continue
                }
                if valueOK && value != strings.ToLower(card.Value) {
                    continue
                }

                finalCards = append(finalCards, card)
            }
            return finalCards, nil
        }
  1. 创建一个名为schema.go的文件,其内容如下:
        package cards

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

        // Setup prepares and returns our card
        // schema
        func Setup() (graphql.Schema, error) {
            cardType := CardType()

            // Schema
            fields := graphql.Fields{
                "cards": &graphql.Field{
                    Type: graphql.NewList(cardType),
                    Args: graphql.FieldConfigArgument{
                        "suit": &graphql.ArgumentConfig{
                            Description: "Filter cards by card suit 
                            (hearts, clubs, diamonds, spades)",
                            Type: graphql.String,
                        },
                        "value": &graphql.ArgumentConfig{
                            Description: "Filter cards by card 
                            value (A-K)",
                            Type: graphql.String,
                        },
                    },
                    Resolve: Resolve,
                },
            }

            rootQuery := graphql.ObjectConfig{Name: "RootQuery", 
            Fields: fields}
            schemaConfig := graphql.SchemaConfig{Query: 
            graphql.NewObject(rootQuery)}
            schema, err := graphql.NewSchema(schemaConfig)

            return schema, err
        }
  1. 导航回graphql目录。
  2. 创建一个名为example的新目录并导航到它。
  3. 创建一个名为main.go的文件,其内容如下:
        package main

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

            "github.com/PacktPublishing/
             Go-Programming-Cookbook-Second-Edition/
             chapter12/graphql/cards"
            "github.com/graphql-go/graphql"
        )

        func main() {
            // grab our schema
            schema, err := cards.Setup()
            if err != nil {
                panic(err)
            }

            // Query
            query := `
            {
                cards(value: "A"){
                    value
                    suit
                }
            }`

            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, err := json.MarshalIndent(r, "", " ")
            if err != nil {
                panic(err)
            }
            fmt.Printf("%s \n", rJSON)
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您应该看到以下输出:

$ go run main.go
{
 "data": {
 "cards": [
 {
 "suit": "Spades",
 "value": "A"
 },
 {
 "suit": "Hearts",
 "value": "A"
 },
 {
 "suit": "Clubs",
 "value": "A"
 },
 {
 "suit": "Diamonds",
 "value": "A"
 }
 ]
 }
} 
  1. 测试一些其他查询,例如以下查询: * cards(suit: "Spades") * cards(value: "3", suit:"Diamonds")
  2. go.mod文件可能会被更新,go.sum文件现在应该出现在顶级配方目录中。
  3. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

cards.go文件定义了一个card对象,并在一个名为cards的全局变量中初始化了基组。这种状态也可以保存在长期存储中,例如数据库。然后我们在types.go中定义CardType,这允许graphql将卡对象解析为响应。接下来,我们跳转到resolve.go,在这里我们定义如何按值和类型过滤卡片。此Resolve函数将由schema.go中定义的最终模式使用。

例如,您可以修改此配方中的Resolve函数,以便从数据库检索数据。最后,我们加载模式并对其运行查询。将我们的模式装载到 REST 端点只是一个小小的修改,但为了简洁起见,这个方法只运行一个硬编码查询。有关GraphQL查询的更多信息,请访问http://graphql.org/learn/queries/