因為之後預計要寫的主題有 React, Vue, Angular, Electron ... 等等的前端框架,所以就先用 Go 架一個 GraphQL API Server 吧。
$ go mod init github.com/wtlin1228/go-gql-server
在根目錄會產生一個 go.mod
// go.mod
module github.com/wtlin1228/go-gql-server
go 1.12
$ go get -u [github.com/gin-gonic/gin](http://github.com/gin-gonic/gin)
$ 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()
}
$ 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
,一切安好,很棒。
$ 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 就表示成功的引入環境變數。
https://github.com/99designs/gqlgen
我們使用這個套件來幫我們建置 graphQL 的基本配置,只需要寫好 schema 和 gqlgen.yml
就可以產出 models, resolvers 以及 graphQL server 的程式碼。
首先建立設定檔 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: []
寫一個 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"
寫一個簡單的 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]
}
跑起來
$ scripts/gqlgen.sh
打開 internal/gql 就可以看到被產生出來的三個檔案,依照我們的 gqlgen.yml 被放置到對應的位置,分別是:
看到有這三個檔案產生久表示我們的 gqlgen 配置成功了,喔耶
將第 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
寫一個 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=/
跑起來
$ 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