(gql-3)graphql-go简介

1、概述

Graphql-go是另外一个基于GO语言实现的graphql服务器框架。
相较于gqlgenGraphql-go需要自己定义每个graphql字段的类型,含义,解析等操作,由于很多细节都需要自己的实现,所以更好理解,上手难度较低,但是相对而言代码量就多了很多。

代码库:https://github.com/graphql-go/graphql
文档:https://pkg.go.dev/github.com/graphql-go/graphql#section-readme
入门示例:https://pkg.go.dev/github.com/graphql-go/graphql#section-readme
本文相关代码:https://github.com/ncfl/graphql

2、简单示例

func helloworld() {
    // Schema 定义查询的Schema
    fields := graphql.Fields{
        "hello": &graphql.Field{
            Type: graphql.String,
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                fmt.Println(p)
                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"}}
}

运行上文的代码,可见,代码定义了查询的字段名为hello,类型是String,解析的时候返回world

当然Graphql-go自身也有提供可视化的查询界面。代码如下

package main

import (
    "fmt"
    "log"
    "net/http"

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


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

type Author struct {
    Name      string
    Tutorials []int
}

type Comment struct {
    Body string
}

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

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

    return tutorials

}

var authorType = graphql.NewObject(
    graphql.ObjectConfig{
        Name: "Author",
        Fields: graphql.Fields{
            "Name": &graphql.Field{
                Type: graphql.String,
            },
            "Tutorials": &graphql.Field{
                Type: graphql.NewList(graphql.Int),
            },
        },
    },
)

var commentType = graphql.NewObject(
    graphql.ObjectConfig{
        Name: "Comment",
        Fields: graphql.Fields{
            "body": &graphql.Field{
                Type: graphql.String,
            },
        },
    },
)

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{
                Type: authorType,
            },
            "comments": &graphql.Field{
                Type: graphql.NewList(commentType),
            },
        },
    },
)

func data() graphql.Schema {

    tutorials := populate()

    // Schema
    fields := graphql.Fields{
        "tutorial": &graphql.Field{
            Type:        tutorialType,
            Description: "Get Tutorial By ID",
            Args: graphql.FieldConfigArgument{
                "id": &graphql.ArgumentConfig{
                    Type: graphql.Int,
                },
            },
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                id, ok := p.Args["id"].(int)
                if ok {
                    // Find tutorial
                    for _, tutorial := range tutorials {
                        if int(tutorial.ID) == id {
                            return tutorial, nil
                        }
                    }
                }
                return nil, nil
            },
        },
        "list": &graphql.Field{
            Type:        graphql.NewList(tutorialType),
            Description: "Get Tutorial List",
            Resolve: func(params graphql.ResolveParams) (interface{}, error) {
                return tutorials, 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)
    }
    return schema
}

func testDemo() {
    schema := data()
    http.Handle("/", handler.New(&handler.Config{
        Schema:     &schema,
        Pretty:     true,
        GraphiQL:   true,
        Playground: true,
    }))
    err := http.ListenAndServe(":8080", nil)
    fmt.Println(err)
}

访问http://localhost:8080/,对应的界面如下:

3、starwar星球大战

原生库完成的星球大战的代码非常不完成,很多功能没有实现,故自己实现了一个比较完成的星球大战,代码可见https://github.com/ncfl/graphql/tree/main/graphql-starwar
星球大战是个很不错的示例,几乎包括了需要用到的所有的细节:

枚举

episodeEnum = graphql.NewEnum(graphql.EnumConfig{
    Name:        "Episode",
    Description: "One of the films in the Star Wars Trilogy",
    Values: graphql.EnumValueConfigMap{
        "NEWHOPE": &graphql.EnumValueConfig{
            Value:       model.EpisodeNewhope,
            Description: "Released in 1977.",
        },
        "EMPIRE": &graphql.EnumValueConfig{
            Value:       model.EpisodeEmpire,
            Description: "Released in 1980.",
        },
        "JEDI": &graphql.EnumValueConfig{
            Value:       model.EpisodeJedi,
            Description: "Released in 1983.",
        },
    },
})

接口

characterInterface = graphql.NewInterface(graphql.InterfaceConfig{
    Name:        "Character",
    Description: "A character in the Star Wars Trilogy",
    Fields: graphql.Fields{
        "id": &graphql.Field{
            Type:        graphql.NewNonNull(graphql.ID),
            Description: "The id of the character.",
        },
        "name": &graphql.Field{
            Type:        graphql.NewNonNull(graphql.String),
            Description: "The name of the character.",
        },
        "appearsIn": &graphql.Field{
            Type:        graphql.NewNonNull(graphql.NewList(graphql.NewNonNull(episodeEnum))),
            Description: "Which movies they appear in.",
        },
    },
    ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object {
        if character, ok := p.Value.(*model.Human); ok {
            if _, ok := data.Humans[character.ID]; ok {
                return humanType
            }
        }
        if character, ok := p.Value.(*model.Droid); ok {
            if _, ok := data.Droids[character.ID]; ok {
                return droidType
            }
        }
        return nil
    },
})

普通结构体

friendsEdgeType = graphql.NewObject(graphql.ObjectConfig{
    Name:        "FriendsEdge",
    Description: "An edge object for a character's friends",
    Fields: graphql.Fields{
        "cursor": &graphql.Field{
            Type:        graphql.NewNonNull(graphql.ID),
            Description: "A cursor used for pagination",
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                if human, ok := p.Source.(*model.FriendsEdge); ok {
                    return human.Cursor, nil
                }
                return nil, nil
            },
        },
        "node": &graphql.Field{
            Type:        characterInterface,
            Description: "The character represented by this friendship edge",
            Resolve: func(p graphql.ResolveParams) (interface{}, error) {
                if human, ok := p.Source.(*model.FriendsEdge); ok {
                    return human.Node, nil
                }
                return nil, nil
            },
        },
    },
})

联合类型

searchResultUnion = graphql.NewUnion(graphql.UnionConfig{
    Name:        "SearchResult",
    Description: "search result",
    Types: []*graphql.Object{
        humanType,
        droidType,
        starShipType,
    },
    ResolveType: func(p graphql.ResolveTypeParams) *graphql.Object {
        if _, ok := p.Value.(*model.Human); ok {
            return humanType
        }
        if _, ok := p.Value.(*model.Droid); ok {
            return droidType
        }
        if _, ok := p.Value.(*model.Starship); ok {
            return starShipType
        }
        return nil
    },
})

输入类型

reviewInputType = graphql.NewInputObject(graphql.InputObjectConfig{
    Name:        "ReviewInput",
    Description: "The input object sent when someone is creating a new review",
    Fields: graphql.InputObjectConfigFieldMap{
        "stars": &graphql.InputObjectFieldConfig{
            Type:        graphql.NewNonNull(graphql.Int),
            Description: "The number of stars this review gave, 1-5",
        },
        "commentary": &graphql.InputObjectFieldConfig{
            Type:        graphql.String,
            Description: "Comment about the movie",
        },
        "time": &graphql.InputObjectFieldConfig{
            Type:        graphql.DateTime,
            Description: "when the review was posted",
        },
    },
})

自定义类型

详见graphql.DateTime

var DateTime = NewScalar(ScalarConfig{
    Name: "DateTime",
    Description: "The `DateTime` scalar type represents a DateTime." +
        " The DateTime is serialized as an RFC 3339 quoted string",
    Serialize:  serializeDateTime,
    ParseValue: unserializeDateTime,
    ParseLiteral: func(valueAST ast.Value) interface{} {
        switch valueAST := valueAST.(type) {
        case *ast.StringValue:
            return unserializeDateTime(valueAST.Value)
        }
        return nil
    },
})

定义完上面的结构体之后,将结构体根据逻辑拼接到QueryMutation之后,便可以启动代码。完成的代码可见上面代码库
试一下执行结果:

4、总结

  • 比较好上手的框架,所有细节都自己实现,可以清晰看到每一步的变化,有助于进一步了解graphql特性
  • 代码量较大,类型,定义,解析等操作都需要自己动手,导致开发量较大
  • 支持的功能和特性有限,相对于gqlgen较轻量较简单
1 声望
1 粉丝
0 条评论
推荐阅读
前端如何入门 Go 语言
类比法是一种学习方法,它是通过将新知识与已知知识进行比较,从而加深对新知识的理解。在学习 Go 语言的过程中,我发现,通过类比已有的前端知识,可以更好地理解 Go 语言的特性。

robin21阅读 2.9k评论 3

封面图
Golang 中 []byte 与 string 转换
string 类型和 []byte 类型是我们编程时最常使用到的数据结构。本文将探讨两者之间的转换方式,通过分析它们之间的内在联系来拨开迷雾。

机器铃砍菜刀22阅读 55.1k评论 1

年度最佳【golang】map详解
这篇文章主要讲 map 的赋值、删除、查询、扩容的具体执行过程,仍然是从底层的角度展开。结合源码,看完本文一定会彻底明白 map 底层原理。

去去100214阅读 11k评论 2

年度最佳【golang】GMP调度详解
Golang最大的特色可以说是协程(goroutine)了, 协程让本来很复杂的异步编程变得简单, 让程序员不再需要面对回调地狱, 虽然现在引入了协程的语言越来越多, 但go中的协程仍然是实现的是最彻底的. 这篇文章将通过分析...

去去100213阅读 11.2k评论 4

【已结束】SegmentFault 思否技术征文丨浅谈 Go 语言框架
亲爱的开发者们:我们的 11 月技术征文如期而来,这次主题围绕 「 Go 」 语言,欢迎大家来参与分享~征文时间11 月 4 日 - 11 月 27 日 23:5911 月 28 日 18:00 前发布中奖名单参与条件新老思否作者均可参加征文...

SegmentFault思否11阅读 4.7k评论 11

封面图
【Go微服务】开发gRPC总共分三步
之前我也有写过RPC相关的文章:《 Go RPC入门指南:RPC的使用边界在哪里?如何实现跨语言调用?》,详细介绍了RPC是什么,使用边界在哪里?并且用Go和php举例,实现了跨语言调用。不了解RPC的同学建议先读这篇文...

王中阳Go8阅读 3.7k评论 6

封面图
【golang】sync.WaitGroup详解
上一期中,我们介绍了 sync.Once 如何保障 exactly once 语义,本期文章我们介绍 package sync 下的另一个工具类:sync.WaitGroup。

去去100213阅读 30.2k评论 2

1 声望
1 粉丝
宣传栏