# 從 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}]"}
    1555 views