iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 1
1
Modern Web

Framework Surfing 30天系列 第 1

Day 1 - API server (Go, GraphQL) - part1

前言

因為之後預計要寫的主題有 React, Vue, Angular, Electron ... 等等的前端框架,所以就先用 Go 架一個 GraphQL API Server 吧。

今天會用到的有:

  1. Gin
  2. GQLGen

今天專案的結構會長這樣:

  • build
  • cmd
    • gql-server
      • main.go:執行 api server ( pkg/server 底下的那個 )
  • internal
    • gql:會把 gqlgen 產生的 graphql server code 複製到這層
      • generated:gqlgen 產生的 graphql server code
      • models:會把 gqlgen 產生的 models 複製到這層
        • generated:gqlgen 產生的 models
      • resolvers:會把 gqlgen 產生的 resolvers 複製到這層
        • generated:gqlgen 產生的 resolvers
    • gql-server
    • handlers:API handler function 都放在這邊
  • pkg
    • server
      • main.go:我們的 api server (Gin)
  • scripts
    • build.sh:產生執行檔
    • run.sh:讀取 .env 黨並且執行 api server

設定 go module:

$ go mod init github.com/wtlin1228/go-gql-server

在根目錄會產生一個 go.mod

// go.mod
module github.com/wtlin1228/go-gql-server

go 1.12

下載最新版本的 Gin:

$ go get -u [github.com/gin-gonic/gin](http://github.com/gin-gonic/gin)

API Server :

用 Gin 來做一個 api server

$ mkdir -p pkg/server

$ touch pkg/server/main.go

// pkg/server/main.go
package server

import (
	"log"

	"github.com/gin-gonic/gin"
)

// Run web server
func Run() {
	host := "localhost"
  port := "7000"
	r := gin.Default()
}

使用 cmd 來啟動 api server

$ mkdir -p cmd/gql-server

$ touch cmd/gql-server/main.go

// cmd/gql-server/main.go
package main

import (
    "github.com/wtlin1228/go-gql-server/pkg/server"
)

func main() {
    server.Run()
}

測試一下,執行 $ go run cmd/gql-server/main.go ,一切安好,很棒。

加上第一個 API - heartbeat

$ mkdir -p internal/handlers

$ touch internal/handlers/heartbeat.go

// internal/handlers/heartbeat.go
package handlers

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

// Heartbeat is simple keep-alive handler
func Heartbeat() gin.HandlerFunc {
	return func(c *gin.Context) {
		c.String(http.StatusOK, "OK")
	}
}

在我們的 pkg/server/main.go 引入這個 heartbeat handler function

// pkg/server/main.go
package server

import (
	"log"

	"github.com/gin-gonic/gin"
	"github.com/wtlin1228/go-gql-server/internal/handlers"
)

// Run web server
func Run() {
	host := "localhost"
  port := "7000"
	r := gin.Default()

	r.GET("/heartbeat", handlers.Heartbeat())
}

測試一下,執行 $ go run cmd/gql-server/main.go ,在瀏覽器中輸入 localhost:7000/heartbeat,如果看到 ok 就表示我們的第一支API實作成功了。

幫我們的專案引入環境變數

$ mkdir pkg/utils

$ touch pkg/utils/env.go

// pkg/utils/env.go
package utils

import (
	"log"
	"os"
	"strconv"
)

// MustGet will return the env or panic if it is not present
func MustGet(k string) string {
	v := os.Getenv(k)
	if v == "" {
		log.Panicln("ENV missing, key: " + k)
	}
	return v
}

// MustGetBool will return the env as boolean or panic if it is not present
func MustGetBool(k string) bool {
	v := os.Getenv(k)
	if v == "" {
		log.Panicln("ENV missing, key: " + k)
	}
	b, err := strconv.ParseBool(v)
	if err != nil {
		log.Panicln("ENV err: [" + k + "]\n" + err.Error())
	}
	return b
}

在根目錄底下加一個放環境變數的檔案

$ touch .env

# Web framework config
GQL_SERVER_HOST=localhost
GQL_SERVER_PORT=7000

寫一個 script 來讀環境變數並且執行 api server

$ mkdir scripts

$ touch scripts/run.sh

$ chmod +x scripts/run.sh

#!/bin/sh
srcPath="cmd"
pkgFile="main.go"
app="gql-server"
src="$srcPath/$app/$pkgFile"

printf "\nStart running: $app\n"
# Set all ENV vars for the server to run
export $(grep -v '^#' .env | xargs) && time go run $src
# This should unset all the ENV vars, just in case.
unset $(grep -v '^#' .env | sed -E 's/(.*)=.*/\1/' | xargs)
printf "\nStopped running: $app\n\n"

改寫一下 pkg/server/main.go

package server

import (
	"log"

	"github.com/gin-gonic/gin"
	"github.com/wtlin1228/go-gql-server/internal/handlers"
	"github.com/wtlin1228/go-gql-server/pkg/utils"
)

var host, port string

func init() {
	host = utils.MustGet("GQL_SERVER_HOST")
	port = utils.MustGet("GQL_SERVER_PORT")
}

// Run web server
func Run() {
	r := gin.Default()

	r.GET("/heartbeat", handlers.Heartbeat())

	// Inform the user where the server is listening
	log.Println("Running @ http://" + host + ":" + port)
	// Print out and exit(1) to the OS if the server cannot run
	log.Fatalln(r.Run(host + ":" + port))
}

測試一下,執行 $ scripts/run.sh ,在瀏覽器中輸入 localhost:7000/heartbeat,如果看到 ok 就表示成功的引入環境變數。

使用 GQLGen

https://github.com/99designs/gqlgen

我們使用這個套件來幫我們建置 graphQL 的基本配置,只需要寫好 schema 和 gqlgen.yml就可以產出 models, resolvers 以及 graphQL server 的程式碼。

  1. 首先建立設定檔 gqlgen.yml,我將 gqlgen 自動產生的檔案都放在每個資料夾的 generated 資料夾內,之後再來將每個檔案複製一份到外面,避免 schema 改了後,重跑 gqlgen 就直接把我們的檔案覆蓋掉。

     # go-gql-server gqlgen.yml file
     # Refer to https://gqlgen.com/config/
     # for detailed .gqlgen.yml documentation.
    
     schema:
       - internal/gql/schemas/schema.graphql
     # Let gqlgen know where to put the generated server
     exec:
       filename: internal/gql/generated/generated.go
       package: gql
     # Let gqlgen know where to put the generated models (if any)
     model:
       filename: internal/gql/models/generated/generated.go
       package: models
     # Let gqlgen know where to put the generated resolvers
     resolver:
       filename: internal/gql/resolvers/generated/generated.go
       type: Resolver
       package: resolvers
     autobind: []
    
  2. 寫一個 script 來跑 gqlgen

    $ touch scripts/gqlgen.sh

    $ chmod +x scripts/gqlgen.sh

    每次要重新跑 gqlgen 的時候,都先把舊的檔案刪掉

     #!/bin/bash
     printf "\nRegenerating gqlgen files\n"
     rm -f internal/gql/generated/generated.go \
         internal/gql/models/generated/generated.go \
         internal/gql/resolvers/generated/generated.go
     time go run -v github.com/99designs/gqlgen $1
     printf "\nDone.\n\n"
    
  3. 寫一個簡單的 schema

    $ mkdir -p internal/gql/schemas

    $ touch internal/gql/schemas/schema.graphql

     # Types
     type User {
       id: ID
       email: String
       userId: String
     }
    
     # Input Types
     input UserInput {
       email: String
       userId: String
     }
    
     # Define mutations here
     type Mutation {
       createUser(input: UserInput!): User!
       updateUser(input: UserInput!): User!
       deleteUser(userId: ID!): Boolean!
     }
    
     # Define queries here
     type Query {
       users(userId: ID): [User]
     }
    
  4. 跑起來

    $ scripts/gqlgen.sh

    打開 internal/gql 就可以看到被產生出來的三個檔案,依照我們的 gqlgen.yml 被放置到對應的位置,分別是:

    • internal/gql/generated/generated.go
    • internal/gql/models/generated/generated.go
    • internal/gql/resolvers/generated/generated.go

    看到有這三個檔案產生久表示我們的 gqlgen 配置成功了,喔耶

使用 GQLGen 產出的檔案來實作

  1. 將第 gqlgen 產生的三個檔案抓到外層並且做一些小修改

    $ cp internal/gql/generated/generated.go internal/gql

    $ mv internal/gql/generated.go internal/gql/main.go

    $ cp internal/gql/models/generated/generated.go internal/gql/models

    $ mv internal/gql/models/generated.go internal/gql/models/users.go

    $ cp internal/gql/resolvers/generated/generated.go internal/gql/resolvers

    $ mv internal/gql/resolvers/generated.go internal/gql/resolvers/main.go

    修改 internal/gql/resolvers/main.go

     package resolvers
    
     import (
     	"context"
    
     	// remeber to change the import path
     	gql "github.com/wtlin1228/go-gql-server/internal/gql"
     	models "github.com/wtlin1228/go-gql-server/internal/gql/models"
     )
    
     // THIS CODE IS A STARTING POINT ONLY. IT WILL NOT BE UPDATED WITH SCHEMA CHANGES.
    
     type Resolver struct{}
    
     func (r *Resolver) Mutation() gql.MutationResolver {
     	return &mutationResolver{r}
     }
     func (r *Resolver) Query() gql.QueryResolver {
     	return &queryResolver{r}
     }
    
     type mutationResolver struct{ *Resolver }
    
     func (r *mutationResolver) CreateUser(ctx context.Context, input models.UserInput) (*models.User, error) {
     	panic("not implemented")
     }
     func (r *mutationResolver) UpdateUser(ctx context.Context, input models.UserInput) (*models.User, error) {
     	panic("not implemented")
     }
     func (r *mutationResolver) DeleteUser(ctx context.Context, userID string) (bool, error) {
     	panic("not implemented")
     }
    
     type queryResolver struct{ *Resolver }
    
     func (r *queryResolver) Users(ctx context.Context, userID *string) ([]*models.User, error) {
       // this is for test purpose
     	var tempID = "ec17af15-e354-440c-a09f-69715fc8b595"
     	var tempEmail = "your@email.com"
     	var tempUserID = "UserID-1"
     	records := []*models.User{
     		&models.User{
     			ID:     &tempID,
     			Email:  &tempEmail,
     			UserID: &tempUserID,
     		},
     	}
     	return records, nil
     }
    

    修改 internal/gql/main.go

     package gql
    
     import (
     	"bytes"
     	"context"
     	"errors"
     	"strconv"
     	"sync"
    
     	"github.com/99designs/gqlgen/graphql"
     	"github.com/99designs/gqlgen/graphql/introspection"
     	"github.com/vektah/gqlparser"
     	"github.com/vektah/gqlparser/ast"
    
     	// remeber to change the import path
     	models "github.com/wtlin1228/go-gql-server/internal/gql/models"
     )
    
     // do not modify rest of the generated code
    
  2. 寫一個 handler function 並在 pkg/server/main.go 裡面加入 gql route

    $ touch internal/handlers/gql.go

     // internal/handlers/gql.go
     package handlers
    
     import (
     	"github.com/99designs/gqlgen/handler"
     	"github.com/gin-gonic/gin"
     	"github.com/wtlin1228/go-gql-server/internal/gql"
     	"github.com/wtlin1228/go-gql-server/internal/gql/resolvers"
     )
    
     // GraphqlHandler defines the GQLGen GraphQL server handler
     func GraphqlHandler() gin.HandlerFunc {
     	// NewExecutableSchema and Config are in the generated.go file
     	c := gql.Config{
     		Resolvers: &resolvers.Resolver{},
     	}
    
     	h := handler.GraphQL(gql.NewExecutableSchema(c))
    
     	return func(c *gin.Context) {
     		h.ServeHTTP(c.Writer, c.Request)
     	}
     }
    
     // PlaygroundHandler Defines the Playground handler to expose our playground
     func PlaygroundHandler(path string) gin.HandlerFunc {
     	h := handler.Playground("Go GraphQL Server", path)
     	return func(c *gin.Context) {
     		h.ServeHTTP(c.Writer, c.Request)
     	}
     }
    
     // pkg/server/main.go
     package server
    
     import (
     	"log"
    
     	"github.com/gin-gonic/gin"
     	"github.com/wtlin1228/go-gql-server/internal/handlers"
     	"github.com/wtlin1228/go-gql-server/pkg/utils"
     )
    
     var host, port, gqlPath, gqlPgPath string
     var isPgEnabled bool
    
     func init() {
     	host = utils.MustGet("GQL_SERVER_HOST")
     	port = utils.MustGet("GQL_SERVER_PORT")
     	gqlPath = utils.MustGet("GQL_SERVER_GRAPHQL_PATH")
     	gqlPgPath = utils.MustGet("GQL_SERVER_GRAPHQL_PLAYGROUND_PATH")
     	isPgEnabled = utils.MustGetBool("GQL_SERVER_GRAPHQL_PLAYGROUND_ENABLED")
     }
    
     // Run web server
     func Run() {
     	endpoint := "http://" + host + ":" + port
    
     	r := gin.Default()
    
     	r.GET("/heartbeat", handlers.Heartbeat())
    
     	// GraphQL handlers
     	// Playground handler
     	if isPgEnabled {
     		r.GET(gqlPgPath, handlers.PlaygroundHandler(gqlPath))
     		log.Println("GraphQL Playground @ " + endpoint + gqlPgPath)
     	}
     	r.POST(gqlPath, handlers.GraphqlHandler())
     	log.Println("GraphQL @ " + endpoint + gqlPath)
    
     	// Inform the user where the server is listening
     	log.Println("Running @ " + endpoint)
     	// Print out and exit(1) to the OS if the server cannot run
     	log.Fatalln(r.Run(host + ":" + port))
     }
    

    由於我們加入了幾個環境變數,需要修改一下 .env

     # .env
     # Web framework config
     GIN_MODE=debug
     GQL_SERVER_HOST=localhost
     GQL_SERVER_PORT=7000
    
     # GQLGen config
     GQL_SERVER_GRAPHQL_PATH=/graphql
     GQL_SERVER_GRAPHQL_PLAYGROUND_ENABLED=true
     GQL_SERVER_GRAPHQL_PLAYGROUND_PATH=/
    
  3. 跑起來

    $ scripts/run.sh

    localhost:7000/ 下一個 query 測試看看

     # Write your query or mutation here
     query {
       users {
         id,
         email,
         userId
       }
     }
    

    如果看到這樣就表示成功了!

     {
       "data": {
         "users": [
           {
             "id": "ec17af15-e354-440c-a09f-69715fc8b595",
             "email": "your@email.com",
             "userId": "UserID-1"
           }
         ]
       }
     }
    

範例程式碼放在 Here

參考文章:

https://dev.to/cmelgarejo/creating-an-opinionated-graphql-server-with-go-part-1-3g3l

https://dev.to/cmelgarejo/creating-an-opinionated-graphql-server-with-go-part-2-46io

https://en.99designs.nl/blog/engineering/gqlgen-a-graphql-server-generator-for-go/?utm_campaign=Master+the+World+of+Golang&utm_medium=email&utm_source=Revue+newsletter


下一篇
Day 2 - API server (Go, GraphQL) - part2
系列文
Framework Surfing 30天6
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
ST 進行式
iT邦新手 4 級 ‧ 2019-09-02 17:43:55

謝謝樓主熱心的分享,
期待後續看到更多的文章~~ ???

唐唐 iT邦新手 5 級 ‧ 2019-09-02 19:45:34 檢舉

/images/emoticon/emoticon34.gif

我要留言

立即登入留言