Go GraphQL入门

admin
admin 2022年03月06日
  • 在其它设备中阅读本文章

为 API 而生(而不是为数据库而生)的查询语言 GraphQL不是一个和传统SQL一样的查询语言,它是位于 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 请求来获得

快速开始

创建一个包含以下内容的文件:main.go

package main

import (
        "log"
        "net/http"

        graphql "github.com/graph-gophers/graphql-go"
        "github.com/graph-gophers/graphql-go/relay"
)

type query struct{}

func (_ *query) Hello() string { return "Hello, world!" }

func main() {
        s := `
                type Query {
                        hello: String!
                }
        `
        schema := graphql.MustParseSchema(s, &query{})
        http.Handle("/query", &relay.Handler{Schema: schema})
        log.Fatal(http.ListenAndServe(":8080", nil))
}

使用 go 运行该程序

go run main.go

使用 curl 测试查看结果

curl -XPOST -d '{"query": "{ hello }"}' localhost:8080/query

将会输出

{"data":{"hello":"Hello, world!"}}

一般流程

1. 定义 schema

const schema string = `
    scalar Long

    schema {
        query: Query
    }

    # 以太坊区块
    type Block {
        number: Long!
        hash: String!
        nonce: Long!
        timestamp: Long!
        difficulty: Long!
    }
   
    type Query {
        block(number: Long, hash: String): Block
    }
`

参考官方入门文档

2. 自定义类型解码接口实现(如果有)

type Long uint64

// ImplementsGraphQLType returns true if Long implements the provided GraphQL type.
func (b Long) ImplementsGraphQLType(name string) bool { return name == "Long" }

// UnmarshalGraphQL unmarshals the provided GraphQL query data.
func (b *Long) UnmarshalGraphQL(input interface{}) error {
    var err error
    switch input := input.(type) {
    case string:
        // uncomment to support hex values
        //if strings.HasPrefix(input, "0x") {
        //    // apply leniency and support hex representations of longs.
        //    value, err := hexutil.DecodeUint64(input)
        //    *b = Long(value)
        //    return err
        //} else {
        value, err := strconv.ParseUint(input, 10, 64)
        *b = Long(value)
        return err
        //}
    case int32:
        *b = Long(input)
    case uint32:
        *b = Long(input)
    case uint64:
        *b = Long(input)
    default:
        err = fmt.Errorf("unexpected type %T for Long", input)
    }
    return err
}

3. 定义解析器

type Block struct {
    ...
}


func (b *Block) Number(ctx context.Context) (Long, error) {
    ...
}

func (b *Block) Hash(ctx context.Context) (string, error) {
    ...
}

func (b *Block) Difficulty(ctx context.Context) (Long, error) {
    ...
}

func (b *Block) Timestamp(ctx context.Context) (Long, error) {
    ...
}

func (b *Block) Nonce(ctx context.Context) (Long, error) {
    ...
}

type Resolver struct{}

func (r *Resolver) Block(ctx context.Context, args struct {
    Number *Long
    Hash   *string
}) (*Block, error) {
    ...
}

如果解析的字段没有错误可以省去 error 返回值

func (b *Block) Number(ctx context.Context) Long {
    ...
}

4. 组合视图 schema 和解析器 resolver 并启动 http 服务

使用包(github.com/graph-gophers/graphql-go)自带的包装器处理前端请求

type Handler struct {
    Schema *graphql.Schema
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var params struct {
        Query         string                 `json:"query"`
        OperationName string                 `json:"operationName"`
        Variables     map[string]interface{} `json:"variables"`
    }
    if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    response := h.Schema.Exec(r.Context(), params.Query, params.OperationName, params.Variables)
    responseJSON, err := json.Marshal(response)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(responseJSON)
}
func main() {
    handler := &relay.Handler{Schema: graphql.MustParseSchema(schema, &Resolver{})}
    http.Handle("/graphql", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

网页查询 IDE

可以利用 GraphiQL 实现网页查询测试结果,先定义 http 处理器

type GraphiQL struct{}

func respond(w http.ResponseWriter, body []byte, code int) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.Header().Set("X-Content-Type-Options", "nosniff")
    w.WriteHeader(code)
    _, _ = w.Write(body)
}

func errorJSON(msg string) []byte {
    buf := bytes.Buffer{}
    fmt.Fprintf(&buf, `{"error": "%s"}`, msg)
    return buf.Bytes()
}

func (h GraphiQL) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.Method != "GET" {
        respond(w, errorJSON("only GET requests are supported"), http.StatusMethodNotAllowed)
        return
    }
    w.Header().Set("Content-Type", "text/html")
    w.Write(graphiql)
}

var graphiql = []byte(`
<!DOCTYPE html>
<html>
    <head>
        <link
                rel="icon"
                type="image/png"
                href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAActpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+QWRvYmUgSW1hZ2VSZWFkeTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KKS7NPQAAB5FJREFUWAm1FmtsnEdxdr/vfC8/mpgEfHYa6gaUJqAihfhVO7UprSokHsn5jKgKiKLGIIEEbSlpJdQLIJw+UFFQUSuBWir1z9nnpEmgCUnkcxPSmDRCgkKpGoJpfXdxHSc4ftzr2x1m9rvPPQdDDSgrfd/O7szOe2YX4H8cGEtY3tFK2Nu7pjMCChbgzVfD11h4XLKAibahL6dbBv+SaRl6LUsw78XBxTG80mEsWSkxu1oM9qmJlkR7UPhPWSDJCzSISw5zXZGxvpMezUp5GmtWQszunpiAKiPPZ20KyCqY1/ncgs4v+IUPwLJvYhzTVIbmvXgvqwAxkImKJHt1yzM+AQLXvdKXy3QevB4R+3O6wIYHSUCIlABEtTO9bf86pmFa7B6xPeHMi3l668p5SQjInbRGQQw0E3FMH4FHaFPoP8USVaveEo9aaH3LsdRh2vsYKqwhMhRBKw82vGbNQbcC9ePL1+PDmwf7iix0N+xmPoafq4TgDDaRYxmLCrBwD5HpSK4vKRVeP9b3ZyaaaE18UaL4KYE5x5afsWxoBgefFfX+jX6pMH9RvSnX2v1YxPP4D3UAHG2hgm80vRp7ns9nWxOb8kIt3HD6C+O8rpRVoYCxHDOtQwOg4QHS1kIb9oHGVQJlN0h8qPF07FFmkG4byouAjEdSO/bwOntr8kGt8EeNJ3uN27O37fse5PT3lVIjUsrL6MB2IVCThMcbx3ofIt7sZeMFExeTubSR3Zq4tVoEdhHSJs30WqjbIS1Zk6/VqzzhmdbBpyn5p1g4W8LMGkajj9GUSfcM/4IVaji+/QdOa7hehKz69xEPsllLkFZY+HdlWhOdLNxrXm5iTK1xPSHEeo4KxTFPzEsFLHH8D914rG+GGWe2Dd9UJav6ZbW1k9ep7rgF3SnTEUXA3hko2fdkowc2M27dk3deomgfLBIPYlJytC4QLzKLZdAoy3QzNTVqksT2y6Oz+YVL1TK4Oo9FYAVIkRFzgH8F/bOiD0cjv4m+hEA9IdXn8HaC4Mjxzx7OdCZH8R14mra6eB9sfUKTj4SCQLUvCHMqN235rKMGV5ZpPCAoSzGOcs2JaFZYVuc8FF5XQl8uCHV75FT0ZT6Q6Ry+02fZ3b7agLF+MGbYmF/Mg+vE14NY1Xnhjv2fZkTkWO+R2VXqc1BrLczp/OtULV0fOLXjHS5LlvkuhzL05oZf+xnMbtv3BLXZIwyPQNx4iRLvrXRXci/vcV/guXJ4dZ/elnwqfctQlnFxoGyhkY2+eCbTlnyCYU8GwzzcHHBhmKl7261X1CEBaIT0QNxJdyQfpLRdHblt4wNMeuhsVpWPvDulqAXQKH5i9f0Ut7pMT/LhOEWc96hfkBEYYnhDU3DJ2SUKMAEPIagRoTSJObF9uF5oHAC/uF/ENxeRrPcai0vt/k1mE+6GeE9eVIlvQwF+yGfL/KiNuMpUnmF4WQUYwX3AEEzjXmqi5yOp6DO8hrM7TeIZ+Orf2X6DY1oU+FeY1D8xJLh8G2bcsgpQ3vqoAU1P3nWouQaDd8mQdS8Tj1B/Z0sZXm6QyxbvAFlj3Us95e7Jbx6/EYScpnP/kjfMwy3DMre6mXVGIVTqiqi1mtVk8blZR78UOdGbQqDLheLMjWc54Yt7KSAaUvRwTyrdMXREvFF6VtRZfgrALNOcm8ixZxe9uOgBLsMPnftUIdM+tBFKcLtwxCeJ7GbdHDJlJ6DHYetX8gHfSTTEB4P9WNBb5JRq0VrfwbxZRuVN61pMt56ICz3elWxAB18OS//Nep4MKeowTOU/zMwo8RaV5fVKhs4WN1DzCjkzJV1jBT9K1TB6oWN4bR89arDMz7iTa1ikepxsy+CXqmXol1fUfJ4qwUfeptsXL1JNTFNWXkfmO5ydi8KXBIMWvCYnmbOWmKXr5zpZhHotSbQGp9YO+qkb3h05E3vBk+nmwJopw5SSdVxRsOjiCGhEXSMCMFdTrAdbPikul35PvWAN1adPgqAGz8Kk1FLTX2hlCyF9pHSIQlwnp+x6/yb1t9zu8LgFszJHt5v0K+TakuPmbFnmog2cXBzfbFtyj1b6O4SQ4BP76Zr1k1Etwoe7Ir+N/dwcfo8f3QnbsYR7yAO/kxICdAH1En+km/WxhtPRXZ4sZrOoQBk2npjcmmwu2ipMz6s/MlG6JflVqrC9pN8VqLK+1nhix4u8/3Z7YjXPRHeJ52z3vm7Mq6eISa0UeF/DK7FB3r/w8eGP0Htg4f1noud5TXgy1g1lpQIGQelGyLjbQk3J7TZr8yT7uxzwSfu+oiwdIL//gTKc+4MUltxL/lpPFn+ebvqByFhswAjid+VgTLNnXcGcyHGuY7PmvWUHZ2hlqXgXDRNfbD/YSE+2MeeWYzjZMmw+p+MYpnuSJy/FjtZ5DCvPuI9SFv5/DI4buZxfwZBuH7pnpu0QprcOztM3N9v2K8x2DH+FcZktB/nSWeJZ3v93Y8VasRubmqBoGKF4g6oBwjIQoi/MMDrqHOMamnMFmv6ziw0T97diTb0zHB7OEe4ZlCjf5X2U8vGm09HnKrPbo78mMwu6mjFn9tV713TtvWpZSCX83wr9J1EKd8CrhC26AAAAAElFTkSuQmCC"
        />
        <link
                rel="stylesheet"
                href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.13.0/graphiql.css"
                integrity="sha384-Qua2xoKBxcHOg1ivsKWo98zSI5KD/UuBpzMIg8coBd4/jGYoxeozCYFI9fesatT0"
                crossorigin="anonymous"
        />
        <script
                src="https://cdnjs.cloudflare.com/ajax/libs/fetch/3.0.0/fetch.min.js"
                integrity="sha384-5B8/4F9AQqp/HCHReGLSOWbyAOwnJsPrvx6C0+VPUr44Olzi99zYT1xbVh+ZanQJ"
                crossorigin="anonymous"
        ></script>
        <script
                src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.5/umd/react.production.min.js"
                integrity="sha384-dOCiLz3nZfHiJj//EWxjwSKSC6Z1IJtyIEK/b/xlHVNdVLXDYSesoxiZb94bbuGE"
                crossorigin="anonymous"
        ></script>
        <script
                src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.5/umd/react-dom.production.min.js"
                integrity="sha384-QI+ql5f+khgo3mMdCktQ3E7wUKbIpuQo8S5rA/3i1jg2rMsloCNyiZclI7sFQUGN"
                crossorigin="anonymous"
        ></script>
        <script
                src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.13.0/graphiql.min.js"
                integrity="sha384-roSmzNmO4zJK9X4lwggDi4/oVy+9V4nlS1+MN8Taj7tftJy1GvMWyAhTNXdC/fFR"
                crossorigin="anonymous"
        ></script>
    </head>
    <body style="width: 100%; height: 100%; margin: 0; overflow: hidden;">
        <div id="graphiql" style="height: 100vh;">Loading...</div>
        <script>
            function fetchGQL(params) {
                return fetch("/graphql", {
                    method: "post",
                    body: JSON.stringify(params),
                    credentials: "include",
                }).then(function (resp) {
                    return resp.text();
                }).then(function (body) {
                    try {
                        return JSON.parse(body);
                    } catch (error) {
                        return body;
                    }
                });
            }
            ReactDOM.render(
                React.createElement(GraphiQL, {fetcher: fetchGQL}),
                document.getElementById("graphiql")
            )
        </script>
    </body>
</html>
`)

然后绑定路由即可

http.Handle("/graphql/ui", GraphiQL{})