用Go Encore加速API開發

Encore是什麼?

Encore是一個很潮的API框架,主要是用來開發微服務,他獨特的API開發方式,讓你可以更快速的開發API,一個API不用幾秒就可以有個原型,讓整體的開發流程可以更快速,也讓你可以更專注在API的邏輯上,再加上他的特色就是type-safe的撰寫風格,讓你可以更容易的debug,減少一些不必要的錯誤。
除此之外,他還提供管理部署階段的雲端平台,只要擁有一個帳號就可以部署你的服務到雲端,並支援AWS,GCP,讓你連結你的服務到雲端更加的方便。
目前支援的語言有Go和TypeScript,還在不斷的更新中,大家可以期待一下。

其實我是在做一個小專案的時候,發現了Encore,看他們Demo影片覺得好酷,就想試一下這個酷東西。
可以去官網看看他們的介紹,還有一些Demo影片,讓你更了解他們的特色。
雖然我才用Encore沒多久,只有用它開發一個小小的專案,但API開發常用的幾個功能都有用到,所以本篇主要會用我的使用經驗帶大家了解用Encore開發大概有甚麼地方要注意,還有一些我覺得Encore可以進步的地方。

重點整理

專案結構

1
2
3
4
5
6
7
8
9
10
11
|── api/
| |── v1/
| | |── v1.go
| | |── v1_test.go
| |── v2/
| | |── v2.go
| | |── v2_test.go
|── go.mod
|── go.sum
|── encore.app

Encore的專案結構大概長這樣,其中api是我們這個專案的名字,v1和v2是兩個不同的服務,裡面有各自的API邏輯和測試,整個架構一看就知道有哪些服務。

每個服務都可以再繼續增加子服務,比如說v1服務裡面還可以有user, post等等,只要在v1資料夾裡面再增加子服務名稱的資料夾就可以了,但是子服務無法定義API。這種結構充分運用到Go的package特性,也可以切分helper function到子資料夾,讓整個專案的結構更加清晰。

API Schema

Encore的API Schema是用來定義API傳入的格式還有輸出的格式,這些都用Go struct來定義,開發起來相對直觀,也保持了type-safe的特性

這是一個簡單的Hello World API

1
2
3
4
5
6
7
8
9
10
11
12
type HelloParams struct {
Name string
}

type HelloResponse struct {
Message string
}

//encore:api public method=GET path=/hello
func Hello(ctx context.Context, p *HelloParams) (*HelloResponse, error) {
return &api.HelloResponse{Message: "Hello, " + p.Name}, nil
}

這裡定義了一個HelloParams和HelloResponse,分別是傳入的參數和輸出的格式,再來就是定義Hello這個API,這裡的註解是用來定義這個API的method和path,這樣Encore就可以知道這個API是什麼method和path了。
我覺得Encore這樣設計是可以解決團隊開發時候帶來的一些問題,比如說API的格式不一致,或是API的path不一致,這樣就可以在開發的時候就定義好,讓大家都可以遵守這個規範。如果自己開發的話可能就會覺得有點麻煩,但是這樣的好處是可以讓你的API更加的一致,也讓你的API更加的易於維護。
這些struct也可以取得http header的資訊,像這樣

1
2
3
4
5
type HelloParams struct {
Name string
UserAgent string `header:"User-Agent"`
Authorization string `header:"Authorization"`
}

這樣就可以取得User-Agent和Authorization的資訊了,給開發者很大的彈性。
可以去官網看看更多的API Schema的用法。

另外,跟大家分享我踩到小小的坑

1
2
3
4
5
6
7
8
9
10
type GetParams struct {
Limit int
Offset int
}
//encore:api public method=GET path=/items
func Get(ctx context.Context, p *GetParams) ([]Item, error) {
// do something
rlog.Debug("DBG", "params", p)
...
}

encore run之後會給你一個測試API的平台,如果你GET請求的API schema長這樣,測試平台上面預設的query string欄位會有LimitOffset給你填入,但依照官方文件的說法

For GET, HEAD and DELETE requests, parameters are read from the query string by default. The query parameter name defaults to the snake-case encoded name of the corresponding struct field (e.g. BlogPost becomes blog_post).

query string的參數名稱是會變snake case的,所以LimitOffset會變成limitoffset。我當初一直用dashboard測API結果都一樣,後來才發現這個問題。改成這樣就可以了

1
2
3
4
type GetParams struct {
Limit int `query:"limit"`
Offset int `query:"offset"`
}

資料庫使用

Encore有自己一套資料庫連線方式,我直接給大家一個範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
var DB = sqldb.NewDatabase("api", sqldb.DatabaseConfig{
Migrations: "./migrations",
})

func DoSomeThing(ctx context.Context, id, title string, done bool) error {
_, err := DB.Exec(ctx,`
INSERT INTO table (id, title, done)
VALUES ($1, $2, $3)
`, id, title, done)

return err
}

func GetSomeThing(ctx context.Context) error {
var item struct {
ID int64
Title string
Done bool
}

err := DB.QueryRow(ctx, `
SELECT id, title, done
FROM table
LIMIT 1
`).Scan(&item.ID, &item.Title, &item.Done)

return err
}

sqldb是Encore提供的資料庫連線包,用encore run執行NewDatabase那行之後,Encore就會知道你的migration資料在哪裡(在這個例子是./migration資料夾),並自動用Docker建立資料庫,想確認資料的話就執行encoer db shell <你專案的名字>,就可以進入資料庫的shell了。
剛剛提到Encore可以在一個專案定義很多服務,Encore預設是每個有用到資料庫的服務都是獨立的,他們會有自己的資料庫,不會互相污染資料,當然也可以在服務之間共享資料庫,取決於開發者。
我自己覺得Encore這樣query資料會不太好用,而且沒有內建query placeholder,沒把輸入欄位清乾淨的話可能會有SQL injection的問題,於是我就改用Gorm來操作資料庫,寫法是用Encore service把資料庫包起來,然後把會用到資料庫的API都放在Service裡面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//encore:service
type Service struct {
db *gorm.DB
}

var DB = sqldb.NewDatabase("api", sqldb.DatabaseConfig{
Migrations: "./migrations",
})

func initService() *Service {
db, err := gorm.Open(postgres.New(postgres.Config{
Conn: DB.Stdlib(),
}))

if err != nil {
return nil, err
}
return &Service{db: db}, nil
}

//encore:api public method=GET path=/hello
func (s *Service) Hello(ctx context.Context, p *HelloParams) (*HelloResponse, error) {
// s.db.... do whatever you want
return &HelloResponse{Message: "Hello, " + p.Name}, nil
}

這裡需要import
"gorm.io/driver/postgres"
"gorm.io/gorm"
"encore.dev/storage/sqldb"這些package
這樣就可以用Gorm來操作資料庫了,寫法比較乾淨,也比較不會有SQL injection的問題,也可以用Gorm的一些特性,比如說Preload,Create,Update等等,讓資料庫操作更加的方便。

快取功能

Encore有內建的快取API,是用Redis來實作的,支援多種資料類型,像是string, int, floats還有struct,這些類型也可以以set或ordered-list的方式存進Redis,還蠻方便的。
這是一個簡單的快取API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var Cluster = cache.NewCluster("api", cache.ClusterConfig{
EvictionPolicy: cache.AllKeysLRU,
})

var testKeySpace = cache.NewStringKeyspace[string](Cluster, cache.KeyspaceConfig{
KeyPattern: "test/:key",
DefaultExpiry: cache.ExpireIn(5 * time.Minute),
})

func SetTest(ctx context.Context, key string, value string) error {
return testKeySpace.Set(ctx, key, value)
}

func GetTest(ctx context.Context, key string) (string, error) {
var value string
err := testKeySpace.Get(ctx, key, &value)
return value, err
}

Encore裡面的快取都是用KeySpace來定義,這樣可以把相同類型的快取放在一起,也可以用KeyPattern來定義快取的key。
這裡跟大家講個我在實作struct型態的快取遇到的bug,如果你有一個像這樣的keySpace

1
2
3
4
5
6
7
8
type Person struct {
Name string
Age int
}
var testKeySpace = cache.NewStructKeyspace[string, Person](Cluster, cache.KeyspaceConfig{
KeyPattern: "test/:key",
DefaultExpiry: cache.ExpireIn(5 * time.Minute),
})

不知道是Encore沒有把一個struct marshal成string的方式寫好還是甚麼原因,我把一個Person struct存到testKeySpace的時候再拿出來,Person裡面的Name和Age都變成了空值,我怎麼試都試不出來,最後只好放棄用struct存快取,改用string的方式存,這樣就沒有問題了。
解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func SetTest(ctx context.Context, key string, value Person) error {
b, err := json.Marshal(value)
if err != nil {
return err
}
return testKeySpace.Set(ctx, key, string(b))
}

func GetTest(ctx context.Context, key string) (Person, error) {
var value string
err := testKeySpace.Get(ctx, key, &value)
if err != nil {
return Person{}, err
}
var p Person
err = json.Unmarshal([]byte(value), &p)
return p, err
}

結語

其實有Encore這個方便的框架真的可以讓API開發更加的快速,也讓API的開發更加的一致,也讓API的開發更加的專注在API的邏輯上,而不是一些不必要的事情上,Encore直接幫後端解決了部署的麻煩,也自帶CI/CD,這個框架這一兩年才出來,StackOverflow還沒有太多的人分享遇到的坑,所以我在寫這篇文章的時候也是希望可以幫助到一些人,也希望Encore越來越好。

  1. Encore是什麼?
  2. 重點整理
  3. 專案結構
  4. API Schema
  5. 資料庫使用
  6. 快取功能
  7. 結語