This tutorial will show you how to implement a GraphQL service using Go and eggql. (See the corresponding GraphQL Star Wars Frontend Tutorial for how you can query this service.) It shows how to implement every feature of a GraphQL service, now including subscriptions.
Remember, this tutorial is about implementing the backend (a GraphQL service), so I'll focus on creating the service, not on using it. It's mainly Go code with a few test queries. We won't look at aliases, fragments, inline fragments, variables, directives, schemas and introspection, but rest assured they work. These things are all covered in the GraphQL Star Wars frontend tutorial if you want to try the queries from that.
You don't need to know a lot about Go to create useful services. Although internally eggql uses reflection, that is hidden except that you need to understand how to use field tags (metadata), but you've probably already encountered this in encoding/decoding of JSON (or gob/xml/yaml/etc). You also don't need to understand GraphQL schema syntax as eggql creates the schema based on your struct fields and their metadata.
You should probably follow this tutorial sequentially as each section builds on the previous, but here are some links if you need to quickly find specific information:
- Basic Types - objects (including nested objects), lists and scalars (String, Int, etc)
- Resolvers and Arguments - a resolver function can have argument(s) to refine the data returned
- Enums - GraphQL has enum types but Go doesn't, so they need to be handled specially
- Mutations - Queries retrieve data, Mutations modify the data
- Input Types - mutations (and queries) sometimes need complex arguments
- Interfaces - similar objects can implement an interface to indicate a common behaviour (polymorphism)
- Unions - like interfaces except the types in the union have no fields in common
- Descriptions - fields, arguments, etc can have a description to be used by query designers
- Viewing Descriptions using Introspection
- Errors - how to handle errors
- Contexts - contexts are used to cancel GraphQL queries - e.g. for a timeout if they take too long to run
- Methods as Resolvers - how a resolver function can easily access data of its parent object
- Custom Scalars - you can add new types to be used in your queries called custom scalars
- Subscriptions - Subscriptions are a powerful way to provide a continuous stream of data
Note: the final code for this tutorial is in the git repo (https://github.com/AndrewWPhillips/eggql/tree/main/example/starwars). It's less than 250 lines of code (and most of that is just data field initialization since all the data is stored in memory). This is much smaller (and I think much simpler) than the equivalent Star Wars example using other GraphQL packages (for Go and even other languages). The complete version is also running right now in GCP (Google Cloud Platform), so you can try any Star Wars queries using GraphIQL, Postman, Curl etc., using the address https://aphillips801-eggql-sw.uc.r.appspot.com/graphql.
GraphQL is all about types - scalar types (int, string, etc), object types composed of fields of other types (a bit like Go structs), lists (a bit like a Go slice or array) and more specialized types like interfaces, unions and input types (which we will get to later).
Traditionally when building a GraphQL service, you first create a schema which defines your types, but with eggql you just use Go structs; the schema is created for you. (To see the generated schema refer to Viewing the Schema in the README.) The first thing you need is a root query which is just like any other GraphQL object type. The fields of the root query define the queries that can be submitted to the GraphQL server.
First we'll add queries returning basic types (scalars, lists and nested objects). Then we'll look at how to implement query arguments, mutations, and more advanced types. We'll also look at the sorts of errors you can get and how to handle them.
To start create a new Go project with one Go file called main.go and add the code below, then build and run it. You may need to run go mod tidy
so that the eggql package is downloaded and added to your go.mod file.
package main
import (
"github.com/andrewwphillips/eggql"
"net/http"
)
type (
Query struct {
Hero Character
}
Character struct {
Name string
Friends []*Character
}
)
func main() {
http.Handle("/graphql", eggql.MustRun(
Query{
Hero: Character{
Name: "R2-D2",
Friends: []*Character{
{Name: "Leia Organa"},
{Name: "Luke Skywalker"},
},
},
}))
http.ListenAndServe(":8080", nil)
}
This program handles the first (hero
) query of the GraphQL Star Wars tutorial (see https://graphql.org/learn/queries/).
To explain the code: the type Query
is the root query, since it's used as the first (only) parameter for MustRun()
. With this service running you can post a hero
query to the server which returns a Character
. The Character
object can be queried for its name and for a list of friends.
To test it try the query below using the address http://localhost:8080/graphql (this address comes from the parameters to Handle
and ListenAndServe
in the code above). Eg using Curl:
curl -XPOST -d '{"query": "{hero {name friends {name}}}"}' localhost:8080/graphql
which sends this GraphQL query:
{
hero {
name
friends {
name
}
}
}
Note If using a tool like Postman to send this query make sure you use an HTTP POST method.
which produces this:
{
"data": {
"hero": {
"name": "R2-D2",
"friends": [
{
"name": "Leia Organa"
},
{
"name": "Luke Skywalker"
}
]
}
}
}
Note that you could recursively query the friends of the friends. You can even query friends of friends of friends ... to any depth, but it is unwise to nest queries too deeply (and some servers limit nesting to 3 or 4 levels). Unfortunately, you can't test this with the above data as Luke and Leia sadly do not have any friends (yet).
The Character
type is used as a GraphQL object since it has fields (sub-queries) within it. Query
is an object, but it is special, being the root query.
The Friends
field provides a list of Character
, in this case implemented using a slice of pointers.
The Name
field has the GraphQL scalar type of String!
because it uses the Go string
type. Similarly, any Go integer types create the GraphQL Int!
type, Go bool => Boolean!
and float32/float64 => Float!
. Note that these types are followed by an exclamation mark (!) which indicates they are not nullable. You can get the nullable version by using pointers or adding nullable
to the field tag (as explained later).
Now we'll look at some more advanced types....
In GraphQL the server code that processes a query is called a resolver. In the above example the "resolver" for the hero
query was just a Character
struct. A more useful and more common thing is for a resolver to be a function (technically a Go closure). Using functions for resolvers has advantages, as it allows:
- efficiency, since the function is not executed unless or until its specific data is required
- use of values which are initially unknown or dynamic (e.g. random numbers as in the README example)
- recursive queries (e.g. friends of friends of ...), which can't be pre-computed (without infinite recursion :)
- and function resolvers can take arguments that can modify or refine the results returned
It's the last advantage that we look at here - resolver arguments. Let's change the hero
resolver to take an argument, by making it a function (with one parameter) and adding a tag string containing an egg key like this. (A tag string is a string following a struct field that is saved as metadata to be queried at run-time using reflection.)
type Query struct {
Hero func(episode int) Character `egg:"hero(episode:Int!=2):Character"`
}
where the parts of the egg tag string are:
hero
is the query name (can be deduced from function nameHero
)episode
is the argument name (required)Int!
is the argument type (can be deduced from the parameter typeint
)2
is the default argument value (optional)Character
is the resolver return type (can be deduced from the function return type)
Note that the string (hero(episode Int!=2):Character
) is in GraphQL schema query format, but only the argument name is required. All you really need is this:
type Query struct {
Hero func(int) Character `egg:"(episode)"`
}
[Technical note: you always need to supply argument name(s). They can't be deduced from the Hero
function parameter name(s) using Go reflection.]
Here is a complete program with the func
resolver. I changed the Hero()
function to return a pointer to Character
; this allows us to return a null value when an invalid episode number has been provided as the argument. A better way than returning NULL (as we will see soon) is to have the resolver function return a 2nd error
value, but the official Star Wars server return NULL to indicate an invalid episode.
package main
import (
"github.com/andrewwphillips/eggql"
"net/http"
)
type (
Query struct {
Hero func(episode int) *Character `egg:"(episode=2)"`
}
Character struct {
Name string
Friends []*Character
}
EpisodeDetails struct {
Name string
HeroId int
}
)
var (
characters = []Character{
{Name: "Luke Skywalker"},
{Name: "Leia Organa"},
{Name: "Han Solo"},
{Name: "R2-D2"},
}
episodes = []EpisodeDetails{
{Name: "A New Hope", HeroId: 0},
{Name: "The Empire Strikes Back", HeroId: 0},
{Name: "Return of the Jedi", HeroId: 3},
}
)
func main() {
// Set up friendships
characters[0].Friends = []*Character{&characters[1], &characters[2], &characters[3]}
characters[1].Friends = []*Character{&characters[0], &characters[2], &characters[3]}
characters[2].Friends = []*Character{&characters[0], &characters[1]}
characters[3].Friends = []*Character{&characters[0], &characters[1]}
http.Handle("/graphql", eggql.MustRun(Query{Hero: func(episode int) *Character {
if episode < 0 || episode >= len(episodes) {
return nil
}
return &characters[episodes[episode].HeroId]
}}))
http.ListenAndServe(":8080", nil)
}
Now try this query:
{
hero(episode: 1) {
name
}
}
In the above code we used an integer to identify episodes. For example, the Hero
function's parameter is episode int
. For this type of data GraphQL provides enums, which are essentially an integer restricted to a set of named values. If you are familiar with GraphQL schemas we can define a new Episode
enum type with 3 values type like this:
enum Episode {
NEWHOPE
EMPIRE
JEDI
}
(Don't worry if you are not familiar with schemas as eggql hides the details.)
The three allowed values for an Episode
(NEWHOPE, EMPIRE, and JEDI) are internally represented by the integers 0, 1, and 2. Because Go does not have a native enum type to support GraphQL enums we just use an integer type and also tell eggql about the corresponding enum name (eg Episode
) and the names of its values (eg NEWHOPE
). To do this you pass a map of string slices as the 1st (optional) parameter to MustRun()
where the map key is the enum name and the slice has the enum values. For example, here is the map for two enums: Episode
and LengthUnit
. (We will use LengthUnit
a bit later.)
var gqlEnums = map[string][]string{
"Episode": {"NEWHOPE", "EMPIRE", "JEDI"},
"LengthUnit": {"METER", "FOOT"},
}
It's simple to change the Hero
resolver to use this Episode
enum as its argument.
Hero func(episode int) *Character `egg:"(episode:Episode=JEDI)"`
If you look closely at the above args
option you can see that the episode
argument now has a type name after the colon (:) which is the enum name (Episode
). The default value is changed from the integer literal 2
to the enum value JEDI
.
Of course, a resolver can also return an enum, or a list of enums. Let's add a new appearsIn
field to the Character
struct which is a list of the movies that the character appears in.
Appears []int `egg:"appearsIn:[Episode]"`
Here the first tag option (appearsIn:[Episode]
) says that the field is called appearsIn
and the type is a list of Episode
. (Square brackets around a type in GraphQL means a list of that type.)
Here's the complete program with the above changes. Note that the gqlEnums
map is now the first parameter to MustRun
.
package main
import (
"github.com/andrewwphillips/eggql"
"net/http"
)
type (
Query struct {
Hero func(episode int) *Character `egg:"(episode:Episode=JEDI)"`
}
Character struct {
Name string
Friends []*Character
Appears []int `egg:"appearsIn:[Episode]"`
}
EpisodeDetails struct {
Name string
HeroId int
}
)
var (
gqlEnums = map[string][]string{
"Episode": {"NEWHOPE", "EMPIRE", "JEDI"},
"LengthUnit": {"METER", "FOOT"},
}
characters = []Character{
{Name: "Luke Skywalker"},
{Name: "Leia Organa"},
{Name: "Han Solo"},
{Name: "R2-D2"},
}
episodes = []EpisodeDetails{
{Name: "A New Hope", HeroId: 0},
{Name: "The Empire Strikes Back", HeroId: 0},
{Name: "Return of the Jedi", HeroId: 3},
}
)
func main() {
// Set up friendships
characters[0].Friends = []*Character{&characters[1], &characters[2], &characters[3]}
characters[1].Friends = []*Character{&characters[0], &characters[2], &characters[3]}
characters[2].Friends = []*Character{&characters[0], &characters[1]}
characters[3].Friends = []*Character{&characters[0], &characters[1]}
// Set up appearances
characters[0].Appears = []int{0, 1, 2}
characters[1].Appears = []int{0, 1, 2}
characters[2].Appears = []int{0, 1, 2}
characters[3].Appears = []int{0, 1, 2}
http.Handle("/graphql", eggql.MustRun(gqlEnums, Query{Hero: func(episode int) *Character {
if episode < 0 || episode >= len(episodes) {
return nil
}
return &characters[episodes[episode].HeroId]
}}))
http.ListenAndServe(":8080", nil)
}
If you run this query:
{
hero(episode: EMPIRE) {
name
appearsIn
}
}
You will see this result:
{
"data": {
"hero": {
"name": "Luke Skywalker",
"appearsIn": [
"NEWHOPE",
"EMPIRE",
"JEDI"
]
}
}
}
Interfaces are an advanced, sometimes useful, feature of GraphQL. Interfaces are a bit like interfaces in the type system of Go, so you may be surprised that eggql does not use Go interfaces to implement GraphQL interfaces. Instead, it uses struct embedding.
To demonstrate interfaces we are going to change the Star Wars example so that the Character
type is an interface and add two new types Human
and Droid
that implement the Character
interface.
type (
Human struct {
Character // embed Character to use it as a GraphQL "interface"
Height float64 // only humans have a height (meters)
}
Droid struct {
Character
PrimaryFunction string
}
)
The above Go code creates two GraphQL types Human
and Droid
which implement the GraphQL Character
interface because they embed the Character
struct. (They also have their own type specific fields.)
No changes are required to the earlier Character
struct, but now it's used as a GraphQL interface
due solely to the fact that it has been embedded in another struct (or two in this case).
If you have a Character
struct (or pointer to one) there is no way in Go to find the struct that embeds it or to even determine that it is embedded in another struct. So to return a Character
(which is either a Human
or a Droid
underneath) we return a Human or Droid as a Go interface{}
and use the egg tag (metadata) to indicate that the GraphQL type - see Character
after the colon (:) in the tage below.
Hero func(episode int) interface{} `egg:"hero(episode:Episode=JEDI):Character"`
Here the metadata says that the field implements a query called hero
returning a Character
(and taking an episode
argument). Of course. you also need to change the implementation of the Hero()
function so that it returns a Human
or a Droid
(as an interface{}
).
This change to the return value causes one further complication that given the Query
type passed to MustRun()
there is no way for eggql to discover (by reflection) the Character
type or even the new Human
and Droid
types. The solution is to add a dummy field with a "blank" name of underscore (_). [Technical note: if you use a zero length array to declare the type, it will take up no space if declared at the start of the struct - eg _ [0]Character
instead of _ Character
. This is usually not important.]
package main
import (
"github.com/andrewwphillips/eggql"
"net/http"
)
type (
Query struct {
Hero func(episode int) interface{} `egg:"hero(episode:Episode=JEDI):Character"`
_ Character
_ Human
_ Droid
}
Character struct {
Name string
Friends []*Character
Appears []int `egg:"appearsIn:[Episode]"`
}
Human struct {
Character
Height float64 // meters
}
Droid struct {
Character
PrimaryFunction string
}
EpisodeDetails struct {
Name string
HeroId int
}
)
var (
gqlEnums = map[string][]string{
"Episode": {"NEWHOPE", "EMPIRE", "JEDI"},
"LengthUnit": {"METER", "FOOT"},
}
humans = []Human{
{Character{Name: "Luke Skywalker"}, 1.67},
{Character{Name: "Leia Organa"}, 1.65},
{Character{Name: "Han Solo"}, 1.85},
{Character{Name: "Chewbacca"}, 2.3},
}
droids = []Droid{
{Character{Name: "R2-D2"}, "Astromech"},
{Character{Name: "C-3PO"}, "Protocol"},
}
episodes = []EpisodeDetails{
{Name: "A New Hope", HeroId: 1000},
{Name: "The Empire Strikes Back", HeroId: 1000},
{Name: "Return of the Jedi", HeroId: 2000},
}
)
func main() {
// Set up friendships
luke := &humans[0].Character
leia := &humans[1].Character
solo := &humans[2].Character
chew := &humans[3].Character
r2d2 := &droids[0].Character
c3po := &droids[1].Character
humans[0].Friends = []*Character{leia, solo, chew, r2d2}
humans[1].Friends = []*Character{luke, solo, r2d2, c3po}
humans[2].Friends = []*Character{chew, leia, luke}
humans[3].Friends = []*Character{solo, luke}
droids[0].Friends = []*Character{c3po, luke, leia}
droids[1].Friends = []*Character{r2d2, leia}
// Set up appearances
humans[0].Appears = []int{0, 1, 2}
humans[1].Appears = []int{0, 1, 2}
humans[2].Appears = []int{0, 1, 2}
humans[3].Appears = []int{0, 1, 2}
droids[0].Appears = []int{0, 1, 2}
droids[1].Appears = []int{0, 1, 2}
http.Handle("/graphql", eggql.MustRun(gqlEnums, Query{Hero: func(episode int) interface{} {
if episode < 0 || episode >= len(episodes) {
return nil
}
ID := episodes[episode].HeroId
if ID >= 2000 {
// droids have IDs starting at 2000
ID -= 2000
if ID >= len(droids) {
return nil
}
return droids[ID]
}
// humans have IDs starting at 1000
ID -= 1000
if ID < 0 || ID >= len(humans) {
return nil
}
return humans[ID]
}}))
http.ListenAndServe(":8080", nil)
}
You can check that this works using a GraphQL query with inline fragments. (See Inline Fragments for details.)
{
hero {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
which will produce JSON output like this:
{
"data": {
"hero": {
"name": "R2-D2",
"primaryFunction": "Astromech"
}
}
}
Since R2-D2 is a droid you get the primaryFunction
field. Now use a different episode as the query parameter, as below.
Note that rather than changing the query in this way it is good practice to "parameterize" any parts that might change using variables - see GraphQL Variables. But that's outside the scope of this tutorial.
{
hero(episode: NEWHOPE) {
name
... on Droid {
primaryFunction
}
... on Human {
height
}
}
}
which returns Luke's modest height:
{
"data": {
"hero": {
"name": "Luke Skywalker",
"height": 1.67
}
}
}
Up till now, we have just been using simple queries, so we have omitted the optional query
keyword at the start of the query. We'll add it now, because it is required for things like a mutation
or to add variables. It also allows naming of queries which can make organising and debugging less confusing.
query FriendsOfFriends{
hero {
name
friends {
name
friends {
name
}
}
}
}
GraphQL is mainly used for receiving information from the backend (server) using queries, but it's sometimes required for a frontend (client) to send information to the backend. This is what Mutations are for. Mutations are syntactically identical to queries. There is a root mutation (by default called Mutation
) in the same way there is a root query (by default called Query
). Just as the root query is passed as the 1st parameter to MustRun()
(2nd parameter if you are using enums) then the root mutation is passed as the next (2nd or 3rd) parameter.
Are mutations necessary? A query could in fact modify data on the backend but that is a bad idea for two reasons:
- It's confusing to clients (and to backend code maintainers).
- Mutations are guaranteed to be executed in sequence (whereas a query may resolve in parallel). If modifications are made using queries the behaviour is undefined and the results unpredictable.
To demonstrate mutations, let's add a CreateReview
mutation that allows clients to submit movie ratings and reviews. We add two slices to the existing EpisodeDetails
struct to store the ratings and reviews. The root mutation has a CreateReview
resolver that takes two arguments, an Episode
(enum) and a ReviewInput
. The new ReviewInput
is used as an input type argument to CreateReview
as explained below.
type (
EpisodeDetails struct {
Name string
HeroId int
reviewMu sync.Mutex
Stars []int // rating (0 to 5)
Commentary []string // review text
}
Mutation struct {
CreateReview func(int, ReviewInput) int `egg:"(episode:Episode,review)"`
}
ReviewInput struct { // input type - see below
Stars int
Commentary string
}
)
You may also have noticed we have added a sync.Mutex
to EpisodeDetails
. This requires a quick explanation.
Up until now all the data structures in our example do not change once the server has been started. Hence, concurrent access from different goroutines does not cause contention. But now that we have added a mutation this completely changes things. We have to ensure that multiple mutations running at the same time are not modifying the same data. Even if a query reads data which could be modified is a data race condition.
An important thing to remember is that GraphQL requests may run in parallel. A GraphQL server would not be of much use if it only processed one client request at a time. This is not immediately obvious, as you don't need to create goroutines -- the Go standard library HTTP handler starts a new goroutine to process every new request.
Since the Stars
and Commentary
slices are modified by the CreateReview
mutation we need to protect them with a mutex. There are many ways to do this. We could just use a single mutex, so if a review is being added for any episode then any other query or mutation of any episode is blocked. This is inefficient, and not very scaleable, so we have used a separate sync.Mutex
for each episode. If we were expecting a large number of queries (and few calls to CreateReview
) a read-write mutex (sync.RWMutex
) would be the best option, but I'll leave that for you to explore as an exercise.
We only need use the new mutex in CreateReview
since that is the only place that Stars
and Commentary
slices are used, at the moment.
CreateReview: func(episode int, review ReviewInput) int {
if episode < 0 || episode >= len(episodes) {
return -1
}
episodes[episode].reviewMu.Lock()
defer episodes[episode].reviewMu.Unlock()
episodes[episode].Stars = append(episodes[episode].Stars, review.Stars)
episodes[episode].Commentary = append(episodes[episode].Commentary, review.Commentary)
return len(episodes[episode].Stars)-1
},
Another new thing here is a struct (ReviewInput
) used as a resolver argument. (This creates an input type in the GraphQL schema.) An input type is similar to a GraphQL object type except that it can only be used as an argument to a mutation (or query). Unlike an object (or interface) type the fields of an input type (like Stars
and Commentary
above) cannot have arguments, but they can have any type including a nested input type.
Note that if you try to use the same Go struct as an input type and an object (or interface) type then eggql will panic with an error like: "can't use Xxx for different GraphQL types (input and object)".
Here is the complete program including the CreateReview
mutation.
package main
import (
"github.com/andrewwphillips/eggql"
"net/http"
)
type (
Query struct {
Hero func(episode int) interface{} `egg:"hero(episode:Episode=JEDI):Character"`
_ Character
_ Human
_ Droid
}
Character struct {
Name string
Friends []*Character
Appears []int `egg:"appearsIn:[Episode]"`
}
Human struct {
Character
Height float64 // meters
}
Droid struct {
Character
PrimaryFunction string
}
EpisodeDetails struct {
Name string
HeroId int
reviewMu sync.Mutex
Stars []int
Commentary []string
}
Mutation struct {
CreateReview func(int, ReviewInput) int `egg:"(episode:Episode,review)"`
}
ReviewInput struct {
Stars int
Commentary string
}
)
var (
gqlEnums = map[string][]string{
"Episode": {"NEWHOPE", "EMPIRE", "JEDI"},
"LengthUnit": {"METER", "FOOT"},
}
humans = []Human{
{Character: Character{Name: "Luke Skywalker"}, Height: 1.67},
{Character: Character{Name: "Leia Organa"}, Height: 1.65},
{Character: Character{Name: "Han Solo"}, Height: 1.85},
{Character: Character{Name: "Chewbacca"}, Height: 2.3},
}
droids = []Droid{
{Character: Character{Name: "R2-D2"}, PrimaryFunction: "Astromech"},
{Character: Character{Name: "C-3PO"}, PrimaryFunction: "Protocol"},
}
episodes = []EpisodeDetails{
{Name: "A New Hope", HeroId: 1000},
{Name: "The Empire Strikes Back", HeroId: 1000},
{Name: "Return of the Jedi", HeroId: 2000},
}
)
func main() {
// Set up friendships
luke := &humans[0].Character
leia := &humans[1].Character
solo := &humans[2].Character
chew := &humans[3].Character
r2d2 := &droids[0].Character
c3po := &droids[1].Character
humans[0].Friends = []*Character{leia, solo, chew, r2d2}
humans[1].Friends = []*Character{luke, solo, r2d2, c3po}
humans[2].Friends = []*Character{chew, leia, luke}
humans[3].Friends = []*Character{solo, luke}
droids[0].Friends = []*Character{c3po, luke, leia}
droids[1].Friends = []*Character{r2d2, leia}
// Set up appearances
humans[0].Appears = []int{0, 1, 2}
humans[1].Appears = []int{0, 1, 2}
humans[2].Appears = []int{0, 1, 2}
humans[3].Appears = []int{0, 1, 2}
droids[0].Appears = []int{0, 1, 2}
droids[1].Appears = []int{0, 1, 2}
http.Handle("/graphql", eggql.MustRun(gqlEnums,
Query{
Hero: func(episode int) interface{} {
if episode < 0 || episode >= len(episodes) {
return nil
}
ID := episodes[episode].HeroId
if ID >= 2000 {
// droids have IDs starting at 2000
ID -= 2000
if ID >= len(droids) {
return nil
}
return droids[ID]
}
// humans have IDs starting at 1000
ID -= 1000
if ID < 0 || ID >= len(humans) {
return nil
}
return humans[ID]
},
},
Mutation{
CreateReview: func(episode int, review ReviewInput) int {
if episode < 0 || episode >= len(episodes) {
return -1
}
episodes[episode].reviewMu.Lock()
defer episodes[episode].reviewMu.Unlock()
episodes[episode].Stars = append(episodes[episode].Stars, review.Stars)
episodes[episode].Commentary = append(episodes[episode].Commentary, review.Commentary)
return len(episodes[episode].Stars)-1
},
},
))
http.ListenAndServe(":8080", nil)
}
To run the CreateReview
mutation send this "query" to the server:
mutation {
createReview(episode: EMPIRE, review: {stars: 5, commentary: "one of the greatest science fiction movies"})
}
The response will return the index into the review list for the episode (or -1 if there was an error). As this is the first review (for JEDI) it returns 0.
{
"data": {
"createReview": 0
}
}
One of the great things about eggql is that you may be able to quickly turn existing software into a GraphQL server. This is because you can often use existing structs, slices and maps to generate GraphQL types, with trivial changes to your code. As long as the field name is capitalized then the field will automatically resolve to its value. A good example of this is the Height
field of the Human
struct we have already seen.
One complication with resolver functions is: How do they access the data of their parent? The first thing is to remember that the Go func
type is more than a function pointer but a closure. So far we have only assigned a function to a resolver func
but they can also be assigned instance methods, in which case it retains a pointer to the instance.
(I tend to think of a Go func
variable like a C function pointer, and when you assigned a Go function, or nil
, to it, it is essentially just a function pointer. But it can be more since a closure retains two things: a function pointer and data pointer.)
Imagine we need to change the height
resolver so that it takes an argument specifying the unit (foot or meter) for the returned value. To use an argument the Height float64
field must be converted to a closure that returns a float64
.
Human struct {
Character
Height func(int) float64 `egg:"(unit:LengthUnit=METER)"`
height float64 // stored as meters
}
Now the Height
resolver is a func
taking a "unit" argument. The unit
argument is of type Unit
- an enum we introduced earlier but did not use. The original Height
field is retained but with an initial lower-case 'h' so that it is not seen as a GraphQL field. (Remember non-capitalized fields are ignored.)
The problem now is how does the Height
closure access the height
field? To do this we introduce a method on the Human
type which I called getHeight()
. This method has the same signature as the Height
closure (func(int) float64
).
func (h *Human) getHeight(unit int) float64 {
switch unit {
case 1: // FOOT
return h.height * 3.28084
}
return h.height // METER
}
This method is assigned to the Height
field. Since we have a slice of Human
s we have to do it for each element of the slice in the initialisation.
humans = []Human{
{Character: Character{Name: "Luke Skywalker"}, height: 1.67},
{Character: Character{Name: "Leia Organa"}, height: 1.65},
{Character: Character{Name: "Han Solo"}, height: 1.85},
{Character: Character{Name: "Chewbacca"}, height: 2.3},
}
....
for i := range humans {
humans[i].Height = (&humans[i]).getHeight // each closure retain an instance pointer to it's human
}
With this change, if you add a unit:FOOT
argument to the height
field of a query like this:
{
hero(episode:NEWHOPE) {
name
... on Human {
height(unit:FOOT)
}
}
}
You will get Luke's height in feet instead of meters:
{
"data": {
"hero": {
"name": "Luke Skywalker",
"height": 5.4790028
}
}
}
Unions in GraphQL are like interfaces, but without any common fields. In fact, you could implement the functionality of a union by using an empty interface, except that GraphQL does not allow empty interfaces. A common use of unions is a query that returns objects of different types without the requirement (as with interfaces) of the objects having anything in common.
In eggql you signify a union in the same way as an interface - by embedding a struct in another object (struct), but in this case the embedded struct
must be empty (in truth, just no exported fields).
In essence, if you embed a struct
your type implements an interface unless the embedded struct is empty, whence your type is added to a union.
We'll demonstrate this by adding a search
query as is seen in the standard Star Wars tutorial. This allows you to search all humans, droids and starships. It returns a list of SearchResult
which is a union of Human | Droid | Starship. (Note that we won't handle starships in this tutorial but the final code in example\StarWars\main.go does.)
We merely need to create an empty SearchResult
struct. Like Character
we embed it in Human
and Droid
. (We could have had the search return a Character
except that we later want it also to be able to return other types like Starship
.)
type (
SearchResult struct {} // union SearchResult = Human | Droid
Human struct {
SearchResult
...
}
Droid struct {
SearchResult
...
}
)
To add the search
query we need to declare a Search() func
in the root Query:
Query struct {
...
// Search implements the resolver: "search(text: String!): [SearchResult]"
Search func(string) []interface{} `egg:"(text):[SearchResult]"`
}
....
http.Handle("/graphql", eggql.MustRun(gqlEnums,
Query{
Hero: ...
Search: Search, // assign Search function to resolver
},
Mutation{...
Search()
returns a slice of interface{}, each of which is either a Human
or a Droid
. The args
option says that the query takes one argument called "text". The square brackets in the GraphQL return type ([SearchResult]
) says that it is a list.
The function that implements the search
query is straightforward. We just return a list of humans etc. stored in a slice of interface{}
.
http.Handle("/graphql", eggql.MustRun(gqlEnums,
Query{
Hero: ...
Search: func(text string) (r []interface{}) {
for _, h := range humans {
if strings.Contains(strings.ToLower(h.Name), strings.ToLower(text)) {
r = append(r, h)
}
}
for _, d := range droids {
if strings.Contains(strings.ToLower(d.Name), strings.ToLower(text)) {
r = append(r, d)
}
}
return
},
},
Mutation ...
The returned objects can be differentiated using inline fragments like this:
{
search(text: "o") {
... on Human {
name
height
}
... on Droid {
name
primaryFunction
}
}
}
which will return everyone with "o" in their name:
{
"data": {
"search": [
{
"name": "Han Solo",
"height": 1.85
},
{
"name": "Leia Organa",
"height": 1.65
},
{
"name": "C-3PO",
"primaryFunction": "Protocol"
}
]
}
}
Here's the complete source code at this point.
package main
import (
"net/http"
"strings"
"sync"
"github.com/andrewwphillips/eggql"
)
type (
Query struct {
_ eggql.TagHolder `egg:"# The root query object"`
Hero func(episode int) interface{} `egg:"hero(episode:Episode=JEDI):Character"`
_ Character
_ Human
_ Droid
// Search implements the resolver: "search(text: String!): [SearchResult]"
Search func(string) []interface{} `egg:"(text):[SearchResult]"`
}
Character struct {
Name string
Friends []*Character
Appears []int `egg:"appearsIn:[Episode]"`
}
SearchResult struct{} // union SearchResult = Human | Droid
Human struct {
Character
SearchResult
Height func(int) float64 `egg:"(unit:LengthUnit=METER)"`
height float64 // stored as meters
}
Droid struct {
Character
SearchResult
PrimaryFunction string
}
EpisodeDetails struct {
Name string
HeroId int
reviewMu sync.Mutex
Stars []int
Commentary []string
}
Mutation struct {
CreateReview func(int, ReviewInput) int `egg:"(episode:Episode,review)"`
}
ReviewInput struct {
Stars int
Commentary string
}
)
var (
gqlEnums = map[string][]string{
"Episode": {"NEWHOPE", "EMPIRE", "JEDI"},
"LengthUnit": {"METER", "FOOT"},
}
humans = []Human{
{Character: Character{Name: "Luke Skywalker"}, height: 1.67},
{Character: Character{Name: "Leia Organa"}, height: 1.65},
{Character: Character{Name: "Han Solo"}, height: 1.85},
{Character: Character{Name: "Chewbacca"}, height: 2.3},
}
droids = []Droid{
{Character: Character{Name: "R2-D2"}, PrimaryFunction: "Astromech"},
{Character: Character{Name: "C-3PO"}, PrimaryFunction: "Protocol"},
}
episodes = []EpisodeDetails{
{Name: "A New Hope", HeroId: 1000},
{Name: "The Empire Strikes Back", HeroId: 1000},
{Name: "Return of the Jedi", HeroId: 2000},
}
)
func main() {
// Set up friendships
luke := &humans[0].Character
leia := &humans[1].Character
solo := &humans[2].Character
chew := &humans[3].Character
r2d2 := &droids[0].Character
c3po := &droids[1].Character
humans[0].Friends = []*Character{leia, solo, chew, r2d2}
humans[1].Friends = []*Character{luke, solo, r2d2, c3po}
humans[2].Friends = []*Character{chew, leia, luke}
humans[3].Friends = []*Character{solo, luke}
droids[0].Friends = []*Character{c3po, luke, leia}
droids[1].Friends = []*Character{r2d2, leia}
// Set up human Height closure
for i := range humans {
humans[i].Height = (&humans[i]).getHeight
}
// Set up appearances
humans[0].Appears = []int{0, 1, 2}
humans[1].Appears = []int{0, 1, 2}
humans[2].Appears = []int{0, 1, 2}
humans[3].Appears = []int{0, 1, 2}
droids[0].Appears = []int{0, 1, 2}
droids[1].Appears = []int{0, 1, 2}
http.Handle("/graphql", eggql.MustRun(gqlEnums,
Query{
Hero: func(episode int) interface{} {
if episode < 0 || episode >= len(episodes) {
return nil
}
ID := episodes[episode].HeroId
if ID >= 2000 {
// droids have IDs starting at 2000
ID -= 2000
if ID >= len(droids) {
return nil
}
return droids[ID]
}
// humans have IDs starting at 1000
ID -= 1000
if ID < 0 || ID >= len(humans) {
return nil
}
return humans[ID]
},
Search: func(text string) (r []interface{}) {
for _, h := range humans {
if strings.Contains(strings.ToLower(h.Name), strings.ToLower(text)) {
r = append(r, h)
}
}
for _, d := range droids {
if strings.Contains(strings.ToLower(d.Name), strings.ToLower(text)) {
r = append(r, d)
}
}
return
},
},
Mutation{
CreateReview: func(episode int, review ReviewInput) int {
if episode < 0 || episode >= len(episodes) {
return -1
}
episodes[episode].reviewMu.Lock()
defer episodes[episode].reviewMu.Unlock()
episodes[episode].Stars = append(episodes[episode].Stars, review.Stars)
episodes[episode].Commentary = append(episodes[episode].Commentary, review.Commentary)
return len(episodes[episode].Stars) - 1 // new review is the last one
},
},
))
http.ListenAndServe(":8080", nil)
}
func (h *Human) getHeight(unit int) float64 {
switch unit {
case 1:
return h.height * 3.28084
}
return h.height
}
A GraphQL schema can have descriptions for its elements to assist the query designer. This can be used by tools to interactively build queries - e.g. PostMan uses introspection to get the description and display information on query arguments etc. (In my experience most GraphQL schemas do not contain many, or any, descriptions but adding them can make you service much more useful to its users.)
With eggql you can include descriptions to the following GraphQL elements in various ways (as discussed below). The description text always begins with a hash character (#).
- Types - objects, interfaces and unions
- Fields (resolvers) and their argument(s)
- Enum types and their values
We use metadata (tags) to attach descriptions to types for adding to the schema. Unfortunately, Go only allows attaching metadata to fields of a struct
, so to add a description to a type we add a special field to the struct
with a name of "_" (single underscore) and a type of eggql.TagHolder
. (The eggql.TagHolder
type has zero size so will not increase the size of your struct if you add it at the top.) Here is an example using the Query
struct:
type Query struct {
_ eggql.TagHolder `egg:"# The root query object"`
This adds the description " The root query object" to the Query
type in the GraphQL schema. The same method is used in structs for input, interface and union types.
For resolvers, you just add the description to the tag (at the end of the egg: key string), preceded by a hash character (#). For example, this adds the description " How tall they are" to the height
field of the Human
type.
Height func(int) float64 `egg:"height(unit:LengthUnit=METER) # How tall they are"`
For resolver arguments, just add the description at the end of each argument. For example, this adds the description "units used for the returned height" to the unit
argument of the height
resolver.
Height func(int) float64 `egg:"height(unit:LengthUnit=METER# units used for the returned height)"`
Descriptions for enums are done a bit differently since enums are just stored as a slice of strings. For both the enum type's name and the enum values you can add a description to the end of the string, preceded by a hash character (#). Eg:
gqlEnums = map[string][]string{
"Unit# Units of spatial measurements": {"METER# metric unit", "FOOT# Imperial (US customary) unit"},
You can check the descriptions in the generated schema by using the GetSchema()
method (see Viewing the Schema in the README). Or you can use introspection to query the running service, for example to get the description of the root query object use this introspection query:
{
__schema {
queryType {
name
description
}
}
}
which should return:
{
"data": {
"__schema": {
"queryType": {
"name": "Query",
"description": "The root query object"
}
}
}
}
Resolver func
s return a single value, but they can optionally return an error
which will be reported in the "errors" section of the query result. For example, in the Hero
function we used above, when there is an error we returned nil
which results in a NULL
Character
being seen in the GraphQL query results. An improvement would be to return an error which could provide an explanation.
There are two error conditions in the Hero
resolver.
- an invalid episode is supplied as the query parameter - this is an error made by the caller (client)
- the hero ID stored in the
EpisodeDetails
is invalid - this is an internal error due to data inconsistency
To distinguish between these errors we add a 2nd (error
) return value to the Hero
function (see the complete program below). Now if there is an error the query will return an error message instead of just a NULL
character.
A critical part of any server in Go is using the context.Context
type. It allows all processing associated with a client request to be expediently and tidily terminated. A common use is a server request timeout in case anything is taking too long or has completely stalled.
Using eggql a resolver function can (optionally) take a 1st parameter of context.Context
. You should use a context if the resolver code makes a library or system call that could block on disk or network I/O such as a database query. You also need a context if you read from or write to a Go chan
and the other end may block. Another case is a computationally intensive resolver that can take a long time, in which case you can check if the context has been cancelled regularly (say at least about once a second).
Of course, even if your resolvers are fast and do not block (as in our Star Wars example code), a client may be able to create queries that take a long time to run if using nested queries and/or long lists - eg, a deeply-nested recursive query like:
{
hero {
name
friends {
name
friends {
...
}
}
}
}
Fortunately, you do not need to handle a context parameter for this as eggql will internally cancel the request and not start any more resolvers if the context
is cancelled. However, you do need to wrap your handler in a timeout handler since Go HTTP handlers do not use timeouts by default. As an example I have added a timeout handler in the code below. I set a timeout of 5 seconds but if you reduce the timeout (to say 1 millisecond or less) then you might see the error:
{
"errors":[
{
"message":"timeout"
}
]
}
Here is the complete code including the call to http.TimeoutHandler
. Using http.TimeoutHandler
and handling Context
parameters can mitigate problems due to poorly designed client queries, server overload or even a DOS attack. (Even better is to use complexity throttling which analyses the client request and does not try to run queries that exceed a complexity limit.)
package main
import (
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/andrewwphillips/eggql"
)
type (
Query struct {
_ eggql.TagHolder `egg:"# The root query object"`
Hero func(episode int) (interface{}, error) `egg:"hero(episode:Episode=JEDI):Character"`
_ Character
_ Human
_ Droid
// Search implements the resolver: "search(text: String!): [SearchResult]"
Search func(string) []interface{} `egg:"(text):[SearchResult]"`
}
Character struct {
Name string
Friends []*Character
Appears []int `egg:"appearsIn:[Episode]"`
}
SearchResult struct{} // union SearchResult = Human | Droid
Human struct {
Character
SearchResult
Height func(int) float64 `egg:"(unit:LengthUnit=METER)"`
height float64 // stored as meters
}
Droid struct {
Character
SearchResult
PrimaryFunction string
}
EpisodeDetails struct {
Name string
HeroId int
reviewMu sync.Mutex
Stars []int
Commentary []string
}
Mutation struct {
CreateReview func(int, ReviewInput) int `egg:"(episode:Episode,review)"`
}
ReviewInput struct {
Stars int
Commentary string
}
)
var (
gqlEnums = map[string][]string{
"Episode": {"NEWHOPE", "EMPIRE", "JEDI"},
"LengthUnit": {"METER", "FOOT"},
}
humans = []Human{
{Character: Character{Name: "Luke Skywalker"}, height: 1.67},
{Character: Character{Name: "Leia Organa"}, height: 1.65},
{Character: Character{Name: "Han Solo"}, height: 1.85},
{Character: Character{Name: "Chewbacca"}, height: 2.3},
}
droids = []Droid{
{Character: Character{Name: "R2-D2"}, PrimaryFunction: "Astromech"},
{Character: Character{Name: "C-3PO"}, PrimaryFunction: "Protocol"},
}
episodes = []EpisodeDetails{
{Name: "A New Hope", HeroId: 1000},
{Name: "The Empire Strikes Back", HeroId: 1000},
{Name: "Return of the Jedi", HeroId: 2000},
}
)
func init() {
// Set up friendships
luke := &humans[0].Character
leia := &humans[1].Character
solo := &humans[2].Character
chew := &humans[3].Character
c3po := &droids[0].Character
r2d2 := &droids[1].Character
humans[0].Friends = []*Character{leia, solo, chew, r2d2}
humans[1].Friends = []*Character{luke, solo, r2d2, c3po}
humans[2].Friends = []*Character{chew, leia, luke}
humans[3].Friends = []*Character{solo, luke}
droids[0].Friends = []*Character{r2d2, leia}
droids[1].Friends = []*Character{c3po, luke, leia}
// Set up human Height closure
for i := range humans {
humans[i].Height = (&humans[i]).getHeight
}
// Set up appearances
humans[0].Appears = []int{0, 1, 2}
humans[1].Appears = []int{0, 1, 2}
humans[2].Appears = []int{0, 1, 2}
humans[3].Appears = []int{0, 1, 2}
droids[0].Appears = []int{0, 1, 2}
droids[1].Appears = []int{0, 1, 2}
}
func main() {
handler := eggql.MustRun(gqlEnums,
Query{
Hero: func(episode int) (interface{}, error) {
if episode < 0 || episode >= len(episodes) {
return nil, fmt.Errorf("episode %d not found", episode)
}
ID := episodes[episode].HeroId
if ID >= 2000 {
// droids have IDs starting at 2000
ID -= 2000
if ID < len(droids) {
return droids[ID], nil
}
} else {
// humans have IDs starting at 1000
ID -= 1000
if ID >= 0 && ID < len(humans) {
return humans[ID], nil
}
}
return nil, fmt.Errorf("internal error: no character with ID %d in episode %d", ID, episode)
},
Search: func(text string) (r []interface{}) {
for _, h := range humans {
if strings.Contains(strings.ToLower(h.Name), strings.ToLower(text)) {
r = append(r, h)
}
}
for _, d := range droids {
if strings.Contains(strings.ToLower(d.Name), strings.ToLower(text)) {
r = append(r, d)
}
}
return
},
},
Mutation{
CreateReview: func(episode int, review ReviewInput) int {
if episode < 0 || episode >= len(episodes) {
return -1
}
episodes[episode].reviewMu.Lock()
defer episodes[episode].reviewMu.Unlock()
episodes[episode].Stars = append(episodes[episode].Stars, review.Stars)
episodes[episode].Commentary = append(episodes[episode].Commentary, review.Commentary)
return len(episodes[episode].Stars) - 1
},
},
)
handler = http.TimeoutHandler(handler, 5*time.Second, `{"errors":[{"message":"timeout"}]}`)
http.Handle("/graphql", handler)
http.ListenAndServe(":8080", nil)
}
// getHeight returns the height of a human
// Parameters
// h (receiver) is a pointer to the Human
// unit is the unit for the return value
func (h *Human) getHeight(unit int) float64 {
switch unit {
case 1:
return h.height * 3.28084
}
return h.height
}
GraphQL supports the creation of custom scalar types. You can easily add custom scalars in eggql which we'll demonstrate using a ReviewTime
type, adding a field to the Review
input type to record when a movie review was written. Note that we could use the similar eggql.Time
which implements the GraphQL Time
type, but we'll create our own custom scalar type to demonstrate how it's done.
All that is required to create a custom scalar is to define a Go type that implements the UnmarshalEGGQL
method. This method is used to convert a specially formatted string into a value of your type. You may also want to implement a MarshalEGGQL
method that performs the reverse operation, but some types can get by without this (e.g. if they implement a String
method).
In our case we will create a new struct type called ReviewTime
that uses the standard Go time.Time
type internally. Here is the complete code for the new type:
type ReviewTime struct{ time.Time }
// UnmarshalEGGQL is called when eggql needs to decode a string to a ReviewTime
func (prt *ReviewTime) UnmarshalEGGQL(in string) error {
tmp, err := time.Parse(time.RFC3339, in)
if err != nil {
return fmt.Errorf("%w error in UnmarshalEGGQL for custom scalar Time decoding %q", err, in)
}
prt.Time = tmp
return nil
}
Note: for your type to be used as a custom scalar you must provide a method with this exact signature: UnmarshalEGGQL(string) error
.
For the above type we don't need to provide the inverse MarshallEGGQL
method. The ReviewTime
type has all the methods of time.Time
because it is embedded in it, including the String() string
method which is used for "marshalling". But if we did provide a method it must have this signature: MarshalEGGQL() (string, error)
.
To use the new type we just add a new ReviewTime
field to ReviewInput
and EpisodeDetails
.
EpisodeDetails struct {
Name string
HeroId int
reviewMu sync.Mutex
Stars []int
Commentary []string
Time []ReviewTime // *** new
}
// ...
ReviewInput struct {
Stars int
Commentary string
Time *ReviewTime // *** new
}
Since Time
is a pointer in ReviewInput
it is nullable which means that it does not need to be provided. In the CreateReview
code we use the current time if the mutation does not provide a time.
CreateReview: func(episode int, review ReviewInput) *EpisodeDetails {
// ...
if review.Time == nil {
episodes[episode].Time = append(episodes[episode].Time, ReviewTime{time.Now()})
} else {
episodes[episode].Time = append(episodes[episode].Time, *review.Time)
}
return len(episodes[episode].Stars)-1
},
Subscriptions are one of the most powerful features of GraphQL. They allow a client to request a stream of data that is sent over a permanent connection (eg. websocket). Setting up Subscriptions is easy - almost exactly the same as Queries and Mutations. For example, there is a root Subscription object, just like the root Query object. The major difference with subscriptions is that the resolvers do not return a value - instead they return a Go channel which sends a stream of values.
In this section we will allow the client to subscribe to new reviews for an episode.
The (new) root Subscription object has one NewReviews
resolver that returns a channel of reviews. It takes two parameters: a context that is used to detect when the subscription is closed, and an enum used as the subscription parameter to determine which reviews we want.
We need a new Review
type to send details of the new review. Note that although this has the same fields as the ReviewInput
struct, we need a new struct as we can't use the same Go struct as a GraphQL input
type and object
type.
Subscription struct {
NewReviews func(context.Context, int) <-chan Review `egg:"(episode:Episode)"`
}
Review struct {
Stars int
Commentary string
Time *ReviewTime
}
To keep track of all subscribers for reviews we need a new field in the EpisodeDetails
struct:
EpisodeDetails struct {
...
reviewReceivers map[chan<- Review]context.Context
}
This new reviewReceivers
map is mainly used for the map key (the chan on which reviews are sent). The map value stores the related context which is cancelled when the subscription ends (eg websocket closed).
The only other changes are:
- implement
NewReviews
subscription - returns a channel where new reviews are sent - modify the
CreateReview
mutation - posts new reviews to the channels
NewReviews: func(ctx context.Context, episode int) <-chan Review {
ch := make(chan Review)
episodes[episode].reviewMu.Lock()
defer episodes[episode].reviewMu.Unlock()
episodes[episode].reviewReceivers[ch] = ctx
return ch
},
...
CreateReview: func(episode int, review ReviewInput) int {
...
if len(episodes[episode].reviewReceivers) > 0 {
out := Review{
Stars: review.Stars,
Commentary: review.Commentary,
Time: review.Time,
}
for ch, ctx := range episodes[episode].reviewReceivers {
if ctx.Err() != nil {
delete(episodes[episode].reviewReceivers, ch)
close(ch)
continue
}
ch <-out
}
}
return len(episodes[episode].Stars) - 1
},
I trust this tutorial has helped you to see how easy it is to create a simple GraphQL server using eggql. You don't have to create, or even understand GraphQL schemas. (Under the hood, a schema is generated for you which you can view if you need to.) Unlike other Go packages, this avoids getting lots of run-time panics when your schema does not match your data types.
However, eggql may not be the best solution for you if you want something comprehensive or more efficient. It does not have any support for databases, such as a dataloader since I wrote it to work with in-memory data. It may also be too slow for heavy load as it uses reflection. See the README for some excellent alternative Go GraphQL packages.
For your reference, here is the final code, as at the end of the tutorial. Further enhancements may be found at https://github.com/AndrewWPhillips/eggql/tree/main/example/starwars.
package main
import (
"context"
"fmt"
"net/http"
"strings"
"sync"
"time"
"github.com/andrewwphillips/eggql"
)
// ReviewTime implements a GraphQL custom scalar used to keep track of when a movie review was posted
type ReviewTime struct{ time.Time } // embed Time so we get String() method (for marshaling)
// UnmarshalEGGQL is called when eggql needs to decode a string to a Time
// The existence of this method signals that this type is a custom scalar/
func (rt *ReviewTime) UnmarshalEGGQL(in string) error {
tmp, err := time.Parse(time.RFC3339, in)
if err != nil {
return fmt.Errorf("%w error in UnmarshalEGGQL for custom scalar Time", err)
}
rt.Time = tmp
return nil
}
type (
Query struct {
_ eggql.TagHolder `egg:"# The root query object"`
Hero func(episode int) (interface{}, error) `egg:"hero(episode:Episode=JEDI):Character"`
_ Character
_ Human
_ Droid
// Search implements the resolver: "search(text: String!): [SearchResult]"
Search func(string) []interface{} `egg:"(text):[SearchResult]"`
}
Character struct {
Name string
Friends []*Character
Appears []int `egg:"appearsIn:[Episode]"`
}
SearchResult struct{} // union SearchResult = Human | Droid
Human struct {
Character
SearchResult
Height func(int) float64 `egg:"(unit:LengthUnit=METER)"`
height float64 // stored as meters
}
Droid struct {
Character
SearchResult
PrimaryFunction string
}
EpisodeDetails struct {
Name string
HeroId int
reviewMu sync.Mutex
Stars []int
Commentary []string
Time []ReviewTime
reviewReceivers map[chan<- Review]context.Context
}
Mutation struct {
CreateReview func(int, ReviewInput) int `egg:"(episode:Episode,review)"`
}
ReviewInput struct {
Stars int
Commentary string
Time *ReviewTime `egg:"# time the review was written - current time is used if NULL"`
}
Subscription struct {
NewReviews func(context.Context, int) <-chan Review `egg:"(episode:Episode)"`
}
Review struct {
Stars int
Commentary string
Time *ReviewTime
}
)
var (
gqlEnums = map[string][]string{
"Episode": {"NEWHOPE", "EMPIRE", "JEDI"},
"LengthUnit": {"METER", "FOOT"},
}
humans = []Human{
{Character: Character{Name: "Luke Skywalker"}, height: 1.67},
{Character: Character{Name: "Leia Organa"}, height: 1.65},
{Character: Character{Name: "Han Solo"}, height: 1.85},
{Character: Character{Name: "Chewbacca"}, height: 2.3},
}
droids = []Droid{
{Character: Character{Name: "R2-D2"}, PrimaryFunction: "Astromech"},
{Character: Character{Name: "C-3PO"}, PrimaryFunction: "Protocol"},
}
episodes = []EpisodeDetails{
{Name: "A New Hope", HeroId: 1000},
{Name: "The Empire Strikes Back", HeroId: 1000},
{Name: "Return of the Jedi", HeroId: 2000},
}
)
func init() {
// Set up friendships
luke := &humans[0].Character
leia := &humans[1].Character
solo := &humans[2].Character
chew := &humans[3].Character
c3po := &droids[0].Character
r2d2 := &droids[1].Character
humans[0].Friends = []*Character{leia, solo, chew, r2d2}
humans[1].Friends = []*Character{luke, solo, r2d2, c3po}
humans[2].Friends = []*Character{chew, leia, luke}
humans[3].Friends = []*Character{solo, luke}
droids[0].Friends = []*Character{r2d2, leia}
droids[1].Friends = []*Character{c3po, luke, leia}
// Set up human Height closure
for i := range humans {
humans[i].Height = (&humans[i]).getHeight
}
// Set up appearances
humans[0].Appears = []int{0, 1, 2}
humans[1].Appears = []int{0, 1, 2}
humans[2].Appears = []int{0, 1, 2}
humans[3].Appears = []int{0, 1, 2}
droids[0].Appears = []int{0, 1, 2}
droids[1].Appears = []int{0, 1, 2}
}
func main() {
handler := eggql.MustRun(gqlEnums,
Query{
Hero: func(episode int) (interface{}, error) {
if episode < 0 || episode >= len(episodes) {
return nil, fmt.Errorf("episode %d not found", episode)
}
ID := episodes[episode].HeroId
if ID >= 2000 {
// droids have IDs starting at 2000
ID -= 2000
if ID < len(droids) {
return droids[ID], nil
}
} else {
// humans have IDs starting at 1000
ID -= 1000
if ID >= 0 && ID < len(humans) {
return humans[ID], nil
}
}
return nil, fmt.Errorf("internal error: no character with ID %d in episode %d", ID, episode)
},
Search: func(text string) (r []interface{}) {
for _, h := range humans {
if strings.Contains(strings.ToLower(h.Name), strings.ToLower(text)) {
r = append(r, h)
}
}
for _, d := range droids {
if strings.Contains(strings.ToLower(d.Name), strings.ToLower(text)) {
r = append(r, d)
}
}
return
},
},
Mutation{
CreateReview: func(episode int, review ReviewInput) int {
if episode < 0 || episode >= len(episodes) {
return -1
}
episodes[episode].reviewMu.Lock()
defer episodes[episode].reviewMu.Unlock()
episodes[episode].Stars = append(episodes[episode].Stars, review.Stars)
episodes[episode].Commentary = append(episodes[episode].Commentary, review.Commentary)
if review.Time == nil {
episodes[episode].Time = append(episodes[episode].Time, ReviewTime{time.Now()})
} else {
episodes[episode].Time = append(episodes[episode].Time, *review.Time)
}
if len(episodes[episode].reviewReceivers) > 0 {
out := Review{
Stars: review.Stars,
Commentary: review.Commentary,
Time: review.Time,
}
for ch, ctx := range episodes[episode].reviewReceivers {
if ctx.Err() != nil {
delete(episodes[episode].reviewReceivers, ch)
continue
}
ch <- out
}
}
return len(episodes[episode].Stars) - 1
},
},
Subscription{
NewReviews: func(ctx context.Context, episode int) <-chan Review {
ch := make(chan Review)
episodes[episode].reviewMu.Lock()
defer episodes[episode].reviewMu.Unlock()
episodes[episode].reviewReceivers[ch] = ctx
return ch
},
},
)
//handler = http.TimeoutHandler(handler, 5*time.Second, `{"errors":[{"message":"timeout"}]}`)
http.Handle("/graphql", handler)
http.ListenAndServe(":8080", nil)
}
// getHeight returns the height of a human
// Parameters
// h (receiver) is a pointer to the Human
// unit is the unit for the return value
func (h *Human) getHeight(unit int) float64 {
switch unit {
case 1:
return h.height * 3.28084
}
return h.height
}