Skip to content

Implementation Summary for gqlgen Request

GraphQL has wonderful features, the most charming features omparing to Restful API are:

  • nested query.
  • get the data nothing more.
  • explicated data type.

Here, I will take a deep look to know how these features are implemented. Before taking an eye in the framework code, we should make a reflection first, how could these features be implemented.

The problems are:

  • what’s the network protocol of GraphQL?
  • the result format after analysis. The query language must have some grammers, how it analyzes I don’t care about as a backend developer and I just treated it as a black box. However, the result of alaysis should be more important as it decides how the server sides trigger the underlying logics(eg, call the handler of implementation).
  • how the implementation is triggered?
  • how the data is filtered?
  • how the nested query works?

Here, I use library 99designs/gqlgen to build my server.

Sequence of a request

The key points are shown as below. Here, library mainly works for network handling, graphql language analysis. The workflow for data is done by the generated code.

The sequence of a request:

https://kroki.io/plantuml/svg/eNptVMFu2zAMvesreNtlTe7GMHTo2mHALkUPOzM2bQmVJVeil2ZfP1KWHWfbKYr4-J74HuH7zJh4Hr35YJmn3ByPk8egN4c2jsdMbzOFlu46h0PC0RicOYZ5PFEy5oXSL0pw9xmWUwPfEk72-Qck7cu8Q3wJ6C-_FSOVdAFRGWYcyITIBMkNVtCtpREbYEtAgQUV-_LnbKMnGChQQqYO2tjRwTwrUQMWQyfVHU4f8ebXR3wE9DkKLAPCODOyi-FgHh8ayDYmbmdWncd3kpOUHmJgepc2S36CPqa_CYEj9C50wuurespwuugTXIKAI5knR757IRYNjoluofIrw_DuyYny7BmwZ3Hrq8sTcmsP5qcIT-rZWQ4F7sbJ0yjmlCkKU--8dlWKDhlFXa8amFz7CvPSqQXoUxyrZKYKJuHQEMwa0T7QhXWfdM1owqyDSEd2eYUNKYrabU41Ve2tgQ2rcvm_K7QxiIrY4LGwFp_Fx2x8jFM9gwur3NL_SQg0zJrSUyhuiW062VXjmsjqb8lWg9GNdFRVVtx2u10IyRZIq31nx3Z51ZqUYr5LRBWgaUE_h1bDMlq4IUnEcwoCEGgxZGXZRhLVGq9s6A2kTLSkXBG75P-nMmLKFmX_bilWU_RKFkEsd1yM249d87lSbK6u61BL26YVimvyu3WaYsgV8-_345wc11U9YftqzL3wyMfoDzSyopo=

Check the questions and find details

Network protocol

GraphQL uses http as the network protocol, more actually, GraphQL only provides one endpoint for all requests, the route and details will be handled in GraphQL library which will route by switch in generated code.

  • How gin manages its router? Differently, like gin, it serves its own route tree and use a high effiency algorithm to find the router. It serves http request and find a handler inside gin layer, could also refer code here.

    func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
      // ignore construction
      engine.handleHTTPRequest(c)
      // ignore return back context
    }
    func (engine *Engine) handleHTTPRequest(c *Context) {
    // Find root of the tree for the given HTTP method
      t := engine.trees
      for i, tl := 0, len(t); i < tl; i++ {
          if t[i].method != httpMethod {
              continue
          }
          root := t[i].root
          // Find route in tree
          value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
          if value.params != nil {
              c.Params = *value.params
          }
          if value.handlers != nil {
              c.handlers = value.handlers
              c.fullPath = value.fullPath
              c.Next()
              c.writermem.WriteHeaderNow()
              return
          }
          if httpMethod != "CONNECT" && rPath != "/" {
              if value.tsr && engine.RedirectTrailingSlash {
                  redirectTrailingSlash(c)
                  return
              }
              if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
                  return
              }
          }
          break
      }
    }
    

  • Time complexity of switch

The switch has a O(n) time complexity in the worst time, and golang compiler doesn’t optimize too much as it’s almost impossible for different archietectures, refer here. The stackoverflow also shows the jump table(map) is more efficient than a switch.

The implemented optimizations:

  1. in order, all non-constant cases arecompiled and tested as if-elses.
  2. groups of larger than 3constant cases are binarydivided and conquered.
  3. 3 or fewer cases are comparedlinearly.

Raised an issue to check with the library maintainer, refer here.

Result of analysis

The body string is analyzed in executor.parseQuery. The QueryDocument is a lot of structs, as I don’t want to know the analysis mechanism, let’s see how the result fields, which are used to find a graphql function.

func (e *Executor) parseQuery(
    ctx context.Context,
    stats *graphql.Stats,
    query string,
) (*ast.QueryDocument, gqlerror.List)

type QueryDocument struct {
    Operations OperationList
    Fragments  FragmentDefinitionList
    Position   *Position `dump:"-"`
}

fields := graphql.CollectFields(ec.OperationContext, sel, queryImplementors)

It’s very common to call graphql.CollectFields to convert selection from fields.

  • The selection and fields look like this for the graphql language above.
    {
      todos {
        id
        text
        done
        user {
          id
          name
        }
      }
    }
    
    Untitled Untitled

In short, the analysis stage parse the language to a struct, which help us to use the generated code. It helps to route the request to proper handler.

Trigger implemetation and support nest query

The diagram:

https://kroki.io/plantuml/svg/eNptUsFuwyAMvfMVvu3U9R5NU6VtlXacetjZAydBIkDBrOrfzyQkbaVdkPXe49l-cMiMicvk1NPIHHO330eHviLPOkz7TOdCXtPOWBwSTkph4eDL9ENJDSmUCAN5SshkQAdD6qRHmhB2r_BVKF070OgccBAdg0FGNeP3guB_KTGgw2u2uWp7S85k5UKIrQbrIVEujtv9FzH4eOtAporj2R09XKSKMhZ5c-txrLdPxB282xyR9Qh9SFBnks2SpdZl1W3oBojJ9-LcdrlYHpepVCOq5nOKrgmslNAXr9kGryrxYJKIS_IiEOkcyOqyrSRde-tYoNA_SuaNKtOtiplesP-6TJjyiE5iebBYQ6mQNzVyy3Nw92u397lZbKkuj7xRwCMtg1SLgxzyf_4AcC3RTg==

The impl is wrapped by the generated code fully, and it’s very common for generated code to use switch to select a proper handler.

For example, you could check how _Query works, it just chooses a properly generated wrapper to call. If the field is user, it will call the generated function _Query_users, which call the implementation and filter the data, then return result.

Note that we don’t execute one by one in for-range fields, we stores the handlers into out and then Dispatch it finally. By this way, it supports the nest query.

func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler {
    fields := graphql.CollectFields(ec.OperationContext, sel, queryImplementors)
    out := graphql.NewFieldSet(fields)
    for i, field := range fields {
        switch field.Name {
        case "__typename":
        case "users":
            field := field

            innerFunc := func(ctx context.Context) (res graphql.Marshaler) {
                                // ignore recover code.
                res = ec._Query_users(ctx, field)
                return res
            }

            rrm := func(ctx context.Context) graphql.Marshaler {
                return ec.OperationContext.RootResolverMiddleware(ctx, innerFunc)
            }

            out.Concurrently(i, func() graphql.Marshaler {
                return rrm(innerCtx)
            })
        case "user": // also cases todos, todo, todoFromUser, __type, __schema
        default:
            panic("unknown field " + strconv.Quote(field.Name))
        }
    }
    out.Dispatch()
    return out
}

Data filter

Picking up data happenes after the implmentation is triggered. How we could choose the certain fields of a object?

The answer is with help of the generated code, it has two steps:

  1. convert the selectionSet to fields.
  2. for-range to pick up the certain fields.

By this way, we can pick up the data easily.

func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj *model.User) graphql.Marshaler {
    fields := graphql.CollectFields(ec.OperationContext, sel, userImplementors)
    out := graphql.NewFieldSet(fields)
    for i, field := range fields {
        switch field.Name {
        case "__typename":
            out.Values[i] = graphql.MarshalString("User")
        case "id":
        case "name":
            out.Values[i] = ec._User_name(ctx, field, obj)
        default:
            panic("unknown field " + strconv.Quote(field.Name))
        }
    }
    out.Dispatch()
    return out
}

Hoping feature

For me, I wish if there is a chain query feature, should be better for developers as it could almost finish their request in one single request.

However, this feature introduces the complexity to the GraphQL language itself, as we usually need to do many other things in with the data.