# 從 JavaScript 到 Golang 的啟發之旅
文章連結:
![](https://i.imgur.com/NL9eulC.png)
https://bit.ly/3mi5If6
---
![](https://i.imgur.com/EsDrbiU.png)
* Node.js、Golang的小後端工程師
* 處理非對稱/對稱加解密、橢圓曲線等加解密等技術稍稍熟悉
* 對微服務容器化技術Docker、K8s有強烈喜愛
---
JavaScript 是一個`動態弱型別`的語言,
Golang 是一個`靜態強型別`的語言,
但在學習的過程中,我常常驚訝...
---
> Golang 這做法真 JavaScript
---
從JavaScript學習到Golang 並不太像學一門完全`新`的語言,
而是像學習一門解決 JavaScript 眾多雷的語言。
---
用 JavaScript 地雷的例子讓你了解到 Golang `為什麼這樣設計`、`此設計的精神為何`,使學習 Golang 更加得順暢。
---
## JavaScript 設計的理念
---
JavaScript 之父 Brendan Eich
![](https://i.imgur.com/AM0PcoR.png)
> JavaScript 是一個 Object Oriented Programming(OOP) 與 Functional Programming(FP) 混合的 Hybrid 語言,目標就是簡單、易用、彈性
---
網頁只能瀏覽的時代
![](https://i.imgur.com/3kGEdSg.png)
---
Brendan Eich 借鑒了[Scheme](https://zh.wikipedia.org/wiki/Scheme)這門 FP 語言可傳遞 function 的`First Class`概念來設計了 JavaScript,可輕鬆傳遞處理 function 使code變得更簡單更短
---
```JavaScript
document.querySelector('input').addEventListener('input', e => {
alert(e.target.value)
})
```
---
Java 大紅大紫的年代,網景希望`JavaScript要有些像Java`,但 Java class 又太複雜,
---
```Java
public class MainActivity extends ActionBarActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onClick(View v) {
Toast.makeText(this, "clicked", Toast.LENGTH_SHORT).show();
}
}
```
---
所以 Brendan Eich 借鑑[Self](https://zh.wikipedia.org/wiki/Self)這門沒有 Class 的原型 OOP 語言,設計出有以下幾種特性的 JavaScript OOP:
- 新物件可由另一個物件複製出來,並且因為`原型鏈`指向原本的屬性
- 物件不受到 Class 規範,可以隨意擴充屬性
---
![](https://i.imgur.com/Y74SutO.png)
---
## Object 我真的猜不透你啊
---
`result.toJSON()`最後被送出時到底為什麼爆炸了?
```JavaScript
const controller = function (req, res, next) => {
try {
result = a(req)
result = b(result)
result = c(result)
result = d(result)
result = e(result)
result = f(result)
result = g(result)
result = h(result)
result = i(result)
res.json(result.toJSON())
} catch (err) {
res.status(500).send('Get error')
}
}
```
---
原來...`e function`裡的一行 code 改了`toJSON()`的行為
> 動態增減屬性在雜亂的 code 中是有害的
---
`res.json()`就是要吃object,其他string或int都不要
> 在雜亂的 code 中無法規範input/output的介面是難過的
---
## 規範過於彈性的 code
---
```JavaScript
function callAPI(caller) {
caller.get("https://api");
}
const caller = {
header: 'header',
get(URL) {
console.log(`${URL} with ${this.header}`)
}
}
callAPI(caller)
// 印出 "https://api with header"
```
---
```JavaScript
function callAPI(caller) {
caller.get("https://api");
}
const caller = {
header: 'header',
get(URL) {
console.log(`${URL} with ${this.header}`)
}
}
caller.header = 123
caller.get = undefined
callAPI(caller)
```
---
![](https://i.imgur.com/ZDbP3YD.png)
![](https://i.imgur.com/yD1atUt.png)
---
而如果是 Golang,他會怎麼做呢?
```Golang
package main
import "fmt"
func callAPI(caller Caller) {
caller.Get("https://api")
}
type Caller struct {
header string
}
func (c Caller) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
func main() {
callAPI(Caller{header: "header"})
}
```
---
嗯?怎麼好像新增的 code 不多
Golang struct 就像 JavaScript 被規範成員的 object,
而 struct 中的 function 則是用 Golang 的`receiver function`來實作,並且無法動態修改。
---
## 今晚我想來點多型
---
> 多型是純 JavaScript 開發者較少聽到的詞
沒有`runtime`,沒有傷害
```JavaScript
callAPI(caller)
callAPI(callerB)
callAPI(callerC)
```
---
但 Golang 呢?
限制了`callAPI()`只能傳入`Caller struct`,這時就無法傳入`CallerB struct`、`CallerC struct`,
```Golang
package main
import "fmt"
func callAPI(caller Caller) {
caller.Get("https://api")
}
type Caller struct {
header string
}
func (c Caller) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
func main() {
callAPI(Caller{header: "header"})
}
```
---
### interface
> 把 struct 所擁有的共同 function 定義出來,只要傳入的 struct 有此 function 就可以傳入,
---
```Golang
package main
import "fmt"
type GetHandler interface {
Get(string)
}
func callAPI(getHandler GetHandler) {
getHandler.Get("https://api")
}
type Caller struct {
header string
}
func (c Caller) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
type CallerB struct {
header string
}
func (c CallerB) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
func main() {
callAPI(Caller{header: "header"})
callAPI(CallerB{header: "header"})
}
```
---
Golang 跟 JavaScript 一樣都在追求
`簡單、易用、彈性`
* function 也擁有`First Class`的特性
* 為了單純不使用經典 OOP 常出現的 Class
---
## 為什麼 Golang 不想要 Class
---
Golang 算是 OOP 嗎?官網的[回答](https://golang.org/doc/faq#types)是`Yes and no`
官網認為 interface 這種`行為判斷`的多型,會比 class 以`階層判斷`的多型來得輕量許多。
以 Java 來解釋:
---
```Java
interface Caller {
public void call();
}
interface OtherCaller {
public void call();
}
class ACaller implements Caller {
public ACaller() {
}
public void call() {
System.out.printf("Call API A");
}
}
class BCaller implements Caller {
public BCaller() {
}
public void call() {
System.out.printf("Call API B");
}
}
class CCaller implements OtherCaller {
public CCaller() {
}
public void call() {
System.out.printf("Call API C");
}
}
public class Main {
public static void main(String[] args) {
doRequest(new ACaller());
doRequest(new BCaller());
// 爆炸!雖然行為相同的介面不同!
doRequest(new CCaller());
}
public static void doRequest(Caller caller) {
caller.call();
}
}
```
---
![](https://i.imgur.com/1Zzfesr.png)
在 Uncle Bob 的 Clean Architecture 一書中,
他認為 OOP 中的`封裝`、`繼承`、`多型`中,
`多型`是最具代表性與實戰效果,
`封裝`、`繼承`事實上不限定於在 OOP 語言出現前就可以做到。
---
Java class 在實現上述三個特性很方便,
但因為 class 嚴謹的規範造成如上功能實現的麻煩,
而 Golang 將`多型`設定為整體 OOP 重點考量之一,
而不侷限在利用 class`封裝`、`繼承`的思維中。
---
## 多型的下一步,具有控制反轉(DIP)的 Clean Arcitature
---
> 如果將整個系統呼叫每層的 interface 定義出來,並將每層以此 interface 注入至下一層,那系統將不再被底層綁架
---
> 將 `GetHandler` 的 interface 定義出來,並將每個 Caller 以此 interface 注入至 `callAPI()`,只不過在系統架構上我們習慣把這些 code 稱為`層`。
---
```Golang
package main
import "fmt"
type GetHandler interface {
Get(string)
}
func callAPI(getHandler GetHandler) {
getHandler.Get("https://api")
}
type Caller struct {
header string
}
func (c Caller) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
type CallerB struct {
header string
}
func (c CallerB) Get(URL string) {
fmt.Println(URL + " with " + c.header)
}
func main() {
callAPI(Caller{header: "header"})
callAPI(CallerB{header: "header"})
}
```
---
### 不再被底層綁架
![](https://i.imgur.com/7E1HiXY.png)
---
![](https://i.imgur.com/DcWGwnr.png)
在 Golang 中我個人使用的 Clean Architecture 結構是學習[bxcodec](https://github.com/bxcodec)的 [go-clean-arch](https://github.com/bxcodec/go-clean-arch),
此結構已經在 Github 上經過許多人驗證。
---
![](https://i.imgur.com/DcWGwnr.png)
### Model 層變為 Repository 層
> 凡從外部進來的事物,都可以放在`Repository`層
---
![](https://i.imgur.com/DcWGwnr.png)
### Controller/Delivery 層多了 Usecase 層
> 業務邏輯放置 Usecase 層。而 Controller/Delivery 只負責交代如何把這些 Usecase 帶給 View 層。
---
![](https://i.imgur.com/DcWGwnr.png)
### View 層變得多樣且彈性
> 要更換任何 View 層只需要重寫 Delivery 層,業務邏輯的 Usecase 是不需要改的
---
某次要從 Restful API 更換成 Websocket 介面時,我直接
![](https://i.imgur.com/F1iY76L.png)
---
![](https://i.imgur.com/DcWGwnr.png)
### 給每層建立介面的 Domain 層
> 一切依賴於domain的高獨立性的架構
---
## 實作 Clean Arcitature
---
### Domain 層 - 規範一切的老大哥
```golang
package domain
import "context"
// Digimon ...
type Digimon struct {
ID string `json:"id"`
Name string `json:"name"`
Status string `json:"status"`
}
// DigimonRepository ...
type DigimonRepository interface {
GetByID(ctx context.Context, id string) (*Digimon, error)
Store(ctx context.Context, d *Digimon) error
UpdateStatus(ctx context.Context, d *Digimon) error
}
// DigimonUsecase ..
type DigimonUsecase interface {
GetByID(ctx context.Context, id string) (*Digimon, error)
Store(ctx context.Context, d *Digimon) error
UpdateStatus(ctx context.Context, d *Digimon) error
}
```
---
### Repository 層 - 任何外部資料都我來管
```golang
// ... 其他程式碼
type postgresqlDigimonRepository struct {
db *sql.DB
}
// NewpostgresqlDigimonRepository ...
func NewpostgresqlDigimonRepository(db *sql.DB) domain.DigimonRepository {
return &postgresqlDigimonRepository{db}
}
func (p *postgresqlDigimonRepository) GetByID(ctx context.Context, id string) (*domain.Digimon, error) {
row := p.db.QueryRow("SELECT id, name, status FROM digimons WHERE id =$1", id)
d := &domain.Digimon{}
if err := row.Scan(&d.ID, &d.Name, &d.Status); err != nil {
logrus.Error(err)
return nil, err
}
return d, nil
}
// ... 其他程式碼
```
---
### Usecase 層 - 業務邏輯的管轄處
```go
// ... 其他程式碼
type digimonUsecase struct {
digimonRepo domain.DigimonRepository
}
// NewDigimonUsecase ...
func NewDigimonUsecase(digimonRepo domain.DigimonRepository) domain.DigimonUsecase {
return &digimonUsecase{
digimonRepo: digimonRepo,
}
}
func (du *digimonUsecase) GetByID(ctx context.Context, id string) (*domain.Digimon, error) {
aDigimon, err := du.digimonRepo.GetByID(ctx, id)
if err != nil {
logrus.Error(err)
return nil, err
}
return aDigimon, nil
}
// ... 其他程式碼
```
---
### Delivery 層 - 交付業務邏輯給引擎的跑腿工
Clean Architecture 的分層重點:
1. 定義好個層介面
2. 依賴注入注入再注入
3. 利用各種注入的實體來實作
---
```go
// ... 其他程式碼
// DigimonHandler ...
type DigimonHandler struct {
DigimonUsecase domain.DigimonUsecase
DietUsecase domain.DietUsecase
}
// NewDigimonHandler ...
func NewDigimonHandler(e *gin.Engine, digimonUsecase domain.DigimonUsecase, dietUsecase domain.DietUsecase) {
handler := &DigimonHandler{
DigimonUsecase: digimonUsecase,
DietUsecase: dietUsecase,
}
e.GET("/api/v1/digimons/:digimonID", handler.GetDigimonByDigimonID)
e.POST("/api/v1/digimons", handler.PostToCreateDigimon)
e.POST("/api/v1/digimons/:digimonID/foster", handler.PostToFosterDigimon)
}
// PostToCreateDigimon ...
func (d *DigimonHandler) PostToCreateDigimon(c *gin.Context) {
var body swagger.DigimonInfoRequest
if err := c.BindJSON(&body); err != nil {
logrus.Error(err)
c.JSON(500, &swagger.ModelError{
Code: 3000,
Message: "Internal error. Parsing failed",
})
return
}
aDigimon := domain.Digimon{
Name: body.Name,
}
if err := d.DigimonUsecase.Store(c, &aDigimon); err != nil {
logrus.Error(err)
c.JSON(500, &swagger.ModelError{
Code: 3000,
Message: "Internal error. Store failed",
})
return
}
c.JSON(200, swagger.DigimonInfo{
Id: aDigimon.ID,
Name: aDigimon.Name,
Status: aDigimon.Status,
})
}
// ... 其他程式碼
```
---
### 最後,把一切透過 `main.go` 跑起來吧!
```go
// ... 其他程式碼
func main() {
logrus.Info("HTTP server started")
restfulHost := viper.GetString("RESTFUL_HOST")
restfulPort := viper.GetString("RESTFUL_PORT")
dbHost := viper.GetString("DB_HOST")
dbDatabase := viper.GetString("DB_DATABASE")
dbUser := viper.GetString("DB_USER")
dbPassword := viper.GetString("DB_PASSWORD")
db, err := sql.Open(
"postgres",
fmt.Sprintf("host=%s user=%s password=%s dbname=%s sslmode=disable", dbHost, dbUser, dbPassword, dbDatabase),
)
if err != nil {
logrus.Fatal(err)
}
if err = db.Ping(); err != nil {
logrus.Fatal(err)
}
r := gin.Default()
digimonRepo := _digmonRepo.NewpostgresqlDigimonRepository(db)
dietRepo := _dietRepo.NewPostgresqlDietRepository(db)
digimonUsecase := _digimonUsecase.NewDigimonUsecase(digimonRepo)
dietUsecase := _dietUsecase.NewDietUsecase(dietRepo)
_digimonHandlerHttpDelivery.NewDigimonHandler(r, digimonUsecase, dietUsecase)
logrus.Fatal(r.Run(restfulHost + ":" + restfulPort))
}
// ... 其他程式碼
```
---
### Thank you!
文章連結:
![](https://i.imgur.com/NL9eulC.png)
https://bit.ly/3mi5If6
{"metaMigratedAt":"2023-06-15T14:41:53.909Z","metaMigratedFrom":"Content","title":"從 JavaScript 到 Golang 的啟發之旅","breaks":true,"contributors":"[{\"id\":\"12cf0277-da7e-4805-b01f-fd5c54a934fb\",\"add\":13705,\"del\":2078}]"}