文章目錄
- 一、 RESTful API 服務
- 1. RESTful API 定義
- 1.1 HTTP Method
- 1.2 RESTful API 規范
- 2. RESTful API 風格示例
- 3. RESTful JSON API
- 4. Gin 框架
- 4.1 導入 Gin 框架
- 4.2 使用 Gin 框架
- 4.2.1 獲取特定的用戶(GET)
- 4.2.2 新增一個用戶(POST)
- 4.2.3 刪除一個用戶(DELETE)
- 4.2.4 修改一個用戶的名字(PATCH)
- 二、 RPC 跨平臺服務
- 1. 定義
- 2. Go語言中的RPC
- 2.1 服務端
- 2.2 客戶端
- 3. 基于 HTTP 的RPC
- 3. JSON RPC 跨平臺通信
- 3.1 基于 TCP 的 JSON RPC
- 3.2 基于 HTTP的JSON RPC
- 三、Go 語言的發展前景
一、 RESTful API 服務
在做項目開發的時候,要善于借助已經有的輪子,讓自己的開發更有效率,也更容易實現。
1. RESTful API 定義
RESTful API 是一套規范,它可以規范如何對服務器上的資源進行操作。和 RESTful API 和密不可分的是 HTTP Method。
1.1 HTTP Method
HTTP Method最常見的就是POST和GET,其實最早在 HTTP 0.9 版本中,只有一個GET方法,該方法是一個冪等方法,用于獲取服務器上的資源。
在 HTTP 1.0 版本中又增加了HEAD和POST方法,其中常用的是 POST 方法,一般用于給服務端提交一個資源,導致服務器的資源發生變化。
隨著網絡越來越復雜,在 HTTP1.1 版本的時候,支持的方法增加到了 9 個,新增的方法有 HEAD、OPTIONS、PUT、DELETE、TRACE、PATCH 和 CONNECT。下面是它們各自的作用:
- GET 方法可請求一個指定資源的表示形式,使用 GET 的請求應該只被用于獲取數據。
- HEAD 方法用于請求一個與 GET 請求的響應相同的響應,但沒有響應體。
- POST 方法用于將實體提交到指定的資源,通常導致服務器上的狀態變化或副作用。
- PUT 方法用于請求有效載荷替換目標資源的所有當前表示。
- DELETE 方法用于刪除指定的資源。
- CONNECT 方法用于建立一個到由目標資源標識的服務器的隧道。
- OPTIONS 方法用于描述目標資源的通信選項。
- TRACE 方法用于沿著到目標資源的路徑執行一個消息環回測試。
- PATCH 方法用于對資源應用部分修改。
HTTP 規范針對每個方法都給出了明確的定義,所以使用的時候也要盡可能地遵循這些定義,這樣在開發中才可以更好地協作。
1.2 RESTful API 規范
RESTful API 規范就是基于 HTTP Method 規范對服務器資源的操作,同時規范了 URL 的樣式和 HTTP Status Code。
在 RESTful API 中,使用的主要是以下五種 HTTP 方法:
- GET,表示讀取服務器上的資源;
- POST,表示在服務器上創建資源;
- PUT,表示更新或者替換服務器上的資源;
- DELETE,表示刪除服務器上的資源;
- PATCH,表示更新 / 修改資源的一部分。
以上 HTTP 方法在 RESTful API 規范中是一個操作,操作的就是服務器的資源,服務器的資源通過特定的 URL 表示。
(1)GET 方法的示例
HTTP GET https://www.flysnow.org/users
HTTP GET https://www.flysnow.org/users/123
上例中
- 第一個表示獲取所有用戶的信息;
- 第二個表示獲取 ID 為 123 用戶的信息。
(2) POST 方法的示例
HTTP POST https://www.flysnow.org/users
該示例表示創建一個用戶,通過 POST 方法給服務器提供創建這個用戶所需的全部信息。
這里 users 是個復數。
(3)PUT 方法的示例
HTTP PUT https://www.flysnow.org/users/123
該示例表示要更新 / 替換 ID 為 123 的這個用戶,在更新的時候,會通過 PUT 方法提供更新這個用戶需要的全部用戶信息。這里 PUT 方法和 POST 方法不太一樣的是,從 URL 上看,PUT 方法操作的是單個資源,比如這里 ID 為 123 的用戶。
如果要更新一個用戶的部分信息,使用 PATCH 方法更恰當。
(4)DELETE 方法的示例
HTTP DELETE https://www.flysnow.org/users/123
DELETE 方法的使用和 PUT 方法一樣,也是操作單個資源,這里是刪除 ID 為 123 的這個用戶。
2. RESTful API 風格示例
Go 語言的一個很大的優勢,就是可以很容易地開發出網絡后臺服務,而且性能快、效率高。Golang 提供了內置的 net/http 包處理 HTTP 請求,讓開發者可以比較方便地開發一個 HTTP 服務。
一個簡單的 HTTP 服務的 Go 語言實現代碼如下所示:
func main() {http.HandleFunc("/users",handleUsers)http.ListenAndServe(":8080", nil)
}
func handleUsers(w http.ResponseWriter, r *http.Request){fmt.Fprintln(w,"ID:1,Name:張三")fmt.Fprintln(w,"ID:2,Name:李四")fmt.Fprintln(w,"ID:3,Name:王五")
}
這個示例運行后,在瀏覽器中輸入 http://localhost:8080/users, 就可以看到如下內容信息:
ID:1,Name:張三
ID:2,Name:李四
ID:3,Name:王五
這并不是一個 RESTful API,因為使用者不僅可以通過 HTTP GET 方法獲得所有的用戶信息,還可以通過 POST、DELETE、PUT 等 HTTP 方法獲得所有的用戶信息,這顯然不符合 RESTful API 的規范。
對以上示例進行修改,使它符合 RESTful API 的規范,修改后的示例代碼如下所示:
func handleUsers(w http.ResponseWriter, r *http.Request){switch r.Method {case "GET":w.WriteHeader(http.StatusOK)fmt.Fprintln(w,"ID:1,Name:張三")fmt.Fprintln(w,"ID:2,Name:李四")fmt.Fprintln(w,"ID:3,Name:王五")default:w.WriteHeader(http.StatusNotFound)fmt.Fprintln(w,"not found")}
}
該示例修改了 handleUsers 函數,在該函數中增加了只在使用 GET 方法時,才獲得所有用戶的信息的判斷,其他情況返回 not found。
3. RESTful JSON API
在項目中最常見的是使用 JSON 格式傳輸信息,也就是提供的 RESTful API 要返回 JSON 內容給使用者。
用上面的示例改造成可以返回 JSON 內容的方式,示例代碼如下所示:
//數據源,類似MySQL中的數據
var users = []User{{ID: 1,Name: "張三"},{ID: 2,Name: "李四"},{ID: 3,Name: "王五"},
}
func handleUsers(w http.ResponseWriter, r *http.Request){switch r.Method {case "GET":users,err:=json.Marshal(users)if err!=nil {w.WriteHeader(http.StatusInternalServerError)fmt.Fprint(w,"{\"message\": \""+err.Error()+"\"}")}else {w.WriteHeader(http.StatusOK)w.Write(users)}default:w.WriteHeader(http.StatusNotFound)fmt.Fprint(w,"{\"message\": \"not found\"}")}
}
//用戶
type User struct {ID intName string
}
從以上代碼可以看到,這次的改造主要是新建了一個 User 結構體,并且使用 users 這個切片存儲所有的用戶,然后在 handleUsers 函數中把它轉化為一個 JSON 數組返回。這樣,就實現了基于 JSON 數據格式的 RESTful API。
運行這個示例,在瀏覽器中輸入 http://localhost:8080/users,可以看到如下信息:
[{"ID":1,"Name":"張三"},{"ID":2,"Name":"李四"},{"ID":3,"Name":"王五"}]
4. Gin 框架
雖然 Go 語言自帶的 net/http 包,可以比較容易地創建 HTTP 服務,但是它也有很多不足:
- 不能單獨地對請求方法(POST、GET 等)注冊特定的處理函數;
- 不支持 Path 變量參數;
- 不能自動對 Path 進行校準;
- 性能一般;
- 擴展性不足;
- ……
基于以上這些不足,出現了很多 Golang Web 框架,如 Mux,Gin、Fiber 等,其中使用最多是 Gin 框架。
4.1 導入 Gin 框架
Gin 框架是一個在 Github 上開源的 Web 框架,封裝了很多 Web 開發需要的通用功能,并且性能也非常高,可以很容易地寫出 RESTful API。
Gin 框架其實是一個模塊,也就是 Go Mod,所以采用 Go Mod 的方法引入即可。
安裝代碼如下:
$ go get -u github.com/gin-gonic/gin
導入代碼如下:
import "github.com/gin-gonic/gin"
4.2 使用 Gin 框架
用 Gin 框架重寫上面的示例,修改的代碼如下所示:
func main() {r:=gin.Default()r.GET("/users", listUser)r.Run(":8080")
}
func listUser(c *gin.Context) {c.JSON(200,users)
}
相比 net/http 包,Gin 框架的代碼非常簡單,通過它的 GET 方法就可以創建一個只處理 HTTP GET 方法的服務,而且輸出 JSON 格式的數據也非常簡單,使用 c.JSON 方法即可。
最后通過 Run 方法啟動 HTTP 服務,監聽在 8080 端口。運行這個示例,在瀏覽器中輸入 http://localhost:8080/users,看到的信息和通過 net/http 包實現的效果是一樣的。
4.2.1 獲取特定的用戶(GET)
如果要獲得特定用戶的信息,需要使用的是 GET 方法,并且 URL 格式如下所示:
http://localhost:8080/users/2
以上示例中的 2 是用戶的 ID,也就是通過 ID 來獲取特定的用戶。
通過 Gin 框架 Path 路徑參數可以實現這個功能,示例代碼如下:
func main() {//省略沒有改動的代碼r.GET("/users/:id", getUser)
}
func getUser(c *gin.Context) {id := c.Param("id")var user Userfound := false//類似于數據庫的SQL查詢for _, u := range users {if strings.EqualFold(id, strconv.Itoa(u.ID)) {user = ufound = truebreak}}if found {c.JSON(200, user)} else {c.JSON(404, gin.H{"message": "用戶不存在",})}
}
在 Gin 框架中,路徑中使用冒號表示 Path 路徑參數,比如示例中的 :id,然后在 getUser 函數中可以通過 c.Param(“id”) 獲取需要查詢用戶的 ID 值。
Param 方法的參數要和 Path 路徑參數中的一致,比如示例中都是 ID。
運行這個示例,通過瀏覽器訪問 http://localhost:8080/users/2,就可以獲得 ID 為 2 的用戶,輸出信息如下所示:
{"ID":2,"Name":"李四"}
假如我們訪問一個不存在的 ID,得到的結果如下所示:
? curl http://localhost:8080/users/99
{"message":"用戶不存在"}%
4.2.2 新增一個用戶(POST)
根據 RESTful API 規范,實現新增使用的是 POST 方法,并且 URL 的格式為 http://localhost:8080/users ,向這個 URL 發送數據,就可以新增一個用戶,然后返回創建的用戶信息。
使用 Gin 框架實現新增一個用戶,示例代碼如下:
func main() {//省略沒有改動的代碼r.POST("/users", createUser)
}
func createUser(c *gin.Context) {name := c.DefaultPostForm("name", "")if name != "" {u := User{ID: len(users) + 1, Name: name}users = append(users, u)c.JSON(http.StatusCreated,u)} else {c.JSON(http.StatusOK, gin.H{"message": "請輸入用戶名稱",})}
}
以上新增用戶的主要邏輯是獲取客戶端上傳的 name 值,然后生成一個 User 用戶,最后把它存儲到 users 集合中,達到新增用戶的目的。
在這個示例中,使用 POST 方法來新增用戶,所以只能通過 POST 方法才能新增用戶成功。
運行這個示例,通過如下命令發送一個新增用戶的請求,查看結果:
? curl -X POST -d 'name=小明' http://localhost:8080/users
{"ID":4,"Name":"小明"}
4.2.3 刪除一個用戶(DELETE)
刪除一個用戶比較簡單,它的 API 格式和獲取一個用戶一樣,但是 HTTP 方法換成了DELETE。示例代碼如下所示:
func main() {//省略沒有修改的代碼r.DELETE("/users/:id", deleteUser)
}
func deleteUser(c *gin.Context) {id := c.Param("id")i := -1//類似于數據庫的SQL查詢for index, u := range users {if strings.EqualFold(id, strconv.Itoa(u.ID)) {i = indexbreak}}if i >= 0 {users = append(users[:i], users[i+1:]...)c.JSON(http.StatusNoContent, "")} else {c.JSON(http.StatusNotFound, gin.H{"message": "用戶不存在",})}
}
這個示例的邏輯就是注冊 DELETE 方法,達到刪除用戶的目的。刪除用戶的邏輯是通過ID 查詢:
- 如果可以找到要刪除的用戶,記錄索引并跳出循環,然后根據索引刪除該用戶;
- 如果找不到要刪除的用戶,則返回 404。
4.2.4 修改一個用戶的名字(PATCH)
修改和刪除一個用戶非常像,實現代碼如下所示:
func main() {//省略沒有修改的代碼r.PATCH("/users/:id",updateUserName)
}
func updateUserName(c *gin.Context) {id := c.Param("id")i := -1//類似于數據庫的SQL查詢for index, u := range users {if strings.EqualFold(id, strconv.Itoa(u.ID)) {i = indexbreak}}if i >= 0 {users[i].Name = c.DefaultPostForm("name",users[i].Name)c.JSON(http.StatusOK, users[i])} else {c.JSON(http.StatusNotFound, gin.H{"message": "用戶不存在",})}
}
邏輯和刪除的差不多的,只不過這里使用的是 PATCH方法。
二、 RPC 跨平臺服務
1. 定義
RPC,也就是遠程過程調用,是分布式系統中不同節點調用的方式(進程間通信),屬于 C/S 模式。RPC 由客戶端發起,調用服務端的方法進行通信,然后服務端把結果返回給客戶端。
RPC的核心有兩個:通信協議和序列化。在 HTTP 2 之前,一般采用自定義 TCP 協議的方式進行通信,HTTP 2 出來后,也有采用該協議的,比如流行的gRPC。
序列化和反序列化是一種把傳輸內容編碼和解碼的方式,常見的編解碼方式有 JSON、Protobuf 等。
在大多數 RPC的架構設計中,都有Client、Client Stub、Server、Server Stub這四個組件,Client 和 Server 之間通過 Socket 進行通信。RPC 架構如下圖所示:
RPC 調用的流程:
- 客戶端(Client)調用客戶端存根(Client Stub),同時把參數傳給客戶端存根;
- 客戶端存根將參數打包編碼,并通過系統調用發送到服務端;
- 客戶端本地系統發送信息到服務器;
- 服務器系統將信息發送到服務端存根(Server Stub);
- 服務端存根解析信息,也就是解碼;
- 服務端存根調用真正的服務端程序(Sever);
- 服務端(Server)處理后,通過同樣的方式,把結果再返回給客戶端(Client)。
RPC 調用常用于大型項目,也就是常說的微服務,而且還會包含服務注冊、治理、監控等功能,是一套完整的體系。
2. Go語言中的RPC
在 Go SDK 中,已經內置了 net/rpc 包來幫助開發者實現 RPC。簡單來說,net/rpc 包提供了通過網絡訪問服務端對象方法的能力。
在實際的項目開發中,使用Go 語言自帶的 RPC 框架并不多,比較常用的是Google的gRPC 框架,它是通過Protobuf 序列化的,是基于 HTTP/2 協議的二進制傳輸,并且支持很多編程語言,效率也比較高。
2.1 服務端
一個 RPC 示例的服務端代碼如下所示:
package server
type MathService struct {
}
type Args struct {A, B int
}
func (m *MathService) Add(args Args, reply *int) error {*reply = args.A + args.Breturn nil
}
在以上代碼中:
- 定義了MathService,用于表示一個遠程服務對象;
- Args 結構體用于表示參數;
- Add 這個方法實現了加法的功能,加法的結果通過 replay這個指針變量返回。
定義好服務對象就可以把它注冊到暴露的服務列表中,以供其他客戶端使用了。在Go 語言中,要注冊一個RPC 服務對象可以通過 RegisterName 方法,示例代碼如下所示:
package main
import ("server""log""net""net/rpc"
)
func main() {rpc.RegisterName("MathService",new(server.MathService))l, e := net.Listen("tcp", ":1234")if e != nil {log.Fatal("listen error:", e)}rpc.Accept(l)
}
以上示例代碼中,通過 RegisterName 函數注冊了一個服務對象,該函數接收兩個參數:
- 服務名稱(MathService);
- 具體的服務對象,也就是剛剛定義好的MathService 這個結構體。
然后通過 net.Listen 函數建立一個TCP 鏈接,在 1234 端口進行監聽,最后通過 rpc.Accept 函數在該 TCP 鏈接上提供 MathService 這個 RPC 服務。現在客戶端就可以看到MathService這個服務以及它的Add 方法了。
在 net/rpc 這個RPC框架時,要想把一個對象注冊為 RPC 服務,可以讓客戶端遠程訪問,那么該對象(類型)的方法必須滿足如下條件:
- 方法的類型是可導出的(公開的);
- 方法本身也是可導出的;
- 方法必須有 2 個參數,并且參數類型是可導出或者內建的;
- 方法必須返回一個 error 類型。
總結來說該方法的格式如下所示:
func (t *T) MethodName(argType T1, replyType *T2) error
這里面的 T1、T2都是可以被 encoding/gob 序列化的。
- 第一個參數 argType 是調用者(客戶端)提供的;
- 第二個參數 replyType是返回給調用者結果,必須是指針類型。
2.2 客戶端
代碼如下所示:
package main
import ("fmt""server""log""net/rpc"
)
func main() {client, err := rpc.Dial("tcp", "localhost:1234")if err != nil {log.Fatal("dialing:", err)}args := server.Args{A:7,B:8}var reply interr = client.Call("MathService.Add", args, &reply)if err != nil {log.Fatal("MathService.Add error:", err)}fmt.Printf("MathService.Add: %d+%d=%d", args.A, args.B, reply)
}
在以上實例代碼中,首先通過 rpc.Dial 函數建立 TCP 鏈接。TCP 鏈接建立成功后,就需要準備遠程方法需要的參數,也就是示例中的args 和 reply。參數準備好之后,就可以通過 Call 方法調用遠程的RPC 服務了。Call 方法有 3 個參數,它們的作用分別如下所示:
- 調用的遠程方法的名字,這里是MathService.Add,點前面的部分是注冊的服務的名稱,點后面的部分是該服務的方法;
- 客戶端為了調用遠程方法提供的參數,示例中是args;
- 為了接收遠程方法返回的結果,必須是一個指針,也就是示例中的 &reply,這樣客戶端就可以獲得服務端返回的結果了。
3. 基于 HTTP 的RPC
RPC 除了可以通過 TCP 協議調用之外,還可以通過HTTP 協議進行調用,而且內置的net/rpc 包已經支持,修改以上示例代碼支持 HTTP 協議的調用,服務端代碼如下所示:
func main() {rpc.RegisterName("MathService", new(server.MathService))rpc.HandleHTTP()//新增的l, e := net.Listen("tcp", ":1234")if e != nil {log.Fatal("listen error:", e)}http.Serve(l, nil)//換成http的服務
}
客戶端修改的代碼如下所示:
func main() {client, err := rpc.DialHTTP("tcp", "localhost:1234")//省略了其他沒有修改的代碼
}
可以看到,只需要把建立鏈接的方法從 Dial 換成 DialHTTP 即可。
Go 語言 net/rpc 包提供的 HTTP 協議的 RPC 還有一個調試的 URL,運行服務端代碼后,在瀏覽器中輸入 http://localhost:1234/debug/rpc 回車,即可看到服務端注冊的RPC 服務,以及每個服務的方法,如下圖所示:
如上圖所示,注冊的 RPC 服務、方法的簽名、已經被調用的次數都可以看到。
3. JSON RPC 跨平臺通信
以上實現的RPC 服務是基于 gob 編碼的,這種編碼在跨語言調用的時候比較困難,當前在微服務架構中,RPC 服務的實現者和調用者都可能是不同的編程語言,因此實現的 RPC 服務要支持多語言的調用。
3.1 基于 TCP 的 JSON RPC
實現跨語言 RPC 服務的核心在于選擇一個通用的編碼,這樣大多數語言都支持,比如常用的JSON。在 Go 語言中,實現一個 JSON RPC 服務非常簡單,只需要使用 net/rpc/jsonrpc 包即可。
以上面的示例為例,改造成支持 JSON的RPC 服務,服務端代碼如下所示:
func main() {rpc.RegisterName("MathService", new(server.MathService))l, e := net.Listen("tcp", ":1234")if e != nil {log.Fatal("listen error:", e)}for {conn, err := l.Accept()if err != nil {log.Println("jsonrpc.Serve: accept:", err.Error())return}//json rpcgo jsonrpc.ServeConn(conn)}
}
從以上代碼可以看到,相比 gob 編碼的RPC 服務,JSON 的 RPC 服務是把鏈接交給了jsonrpc.ServeConn這個函數處理,達到了基于 JSON 進行 RPC 調用的目的。
JSON RPC 的客戶端代碼修改的部分如下所示:
func main() {client, err := jsonrpc.Dial("tcp", "localhost:1234")//省略了其他沒有修改的代碼
}
從以上代碼可以看到,只需要把建立鏈接的 Dial方法換成 jsonrpc 包中的即可。
3.2 基于 HTTP的JSON RPC
Go 語言內置的jsonrpc 并沒有實現基于 HTTP的傳輸,需要自己實現,這里參考 gob 編碼的HTTP RPC 實現方式,來實現基于 HTTP的JSON RPC 服務。
RPC 服務端代碼如下所示:
func main() {rpc.RegisterName("MathService", new(server.MathService))//注冊一個path,用于提供基于http的json rpc服務http.HandleFunc(rpc.DefaultRPCPath, func(rw http.ResponseWriter, r *http.Request) {conn, _, err := rw.(http.Hijacker).Hijack()if err != nil {log.Print("rpc hijacking ", r.RemoteAddr, ": ", err.Error())return}var connected = "200 Connected to JSON RPC"io.WriteString(conn, "HTTP/1.0 "+connected+"\n\n")jsonrpc.ServeConn(conn)})l, e := net.Listen("tcp", ":1234")if e != nil {log.Fatal("listen error:", e)}http.Serve(l, nil)//換成http的服務
}
以上代碼的實現基于 HTTP 協議的核心,即使用 http.HandleFunc 注冊了一個 path,對外提供基于 HTTP 的 JSON RPC 服務。在這個 HTTP 服務的實現中,通過Hijack方法劫持鏈接,然后轉交給 jsonrpc 處理,這樣就實現了基于 HTTP 協議的 JSON RPC 服務。
客戶端調用的代碼如下所示:
func main() {client, err := DialHTTP("tcp", "localhost:1234")if err != nil {log.Fatal("dialing:", err)}args := server.Args{A:7,B:8}var reply interr = client.Call("MathService.Add", args, &reply)if err != nil {log.Fatal("MathService.Add error:", err)}fmt.Printf("MathService.Add: %d+%d=%d", args.A, args.B, reply)}// DialHTTP connects to an HTTP RPC server at the specified network address// listening on the default HTTP RPC path.func DialHTTP(network, address string) (*rpc.Client, error) {return DialHTTPPath(network, address, rpc.DefaultRPCPath)}// DialHTTPPath connects to an HTTP RPC server// at the specified network address and path.func DialHTTPPath(network, address, path string) (*rpc.Client, error) {var err errorconn, err := net.Dial(network, address)if err != nil {return nil, err}io.WriteString(conn, "GET "+path+" HTTP/1.0\n\n")// Require successful HTTP response// before switching to RPC protocol.resp, err := http.ReadResponse(bufio.NewReader(conn), &http.Request{Method: "GET"})connected := "200 Connected to JSON RPC"if err == nil && resp.Status == connected {return jsonrpc.NewClient(conn), nil}if err == nil {err = errors.New("unexpected HTTP response: " + resp.Status)}conn.Close()return nil, &net.OpError{Op: "dial-http",Net: network + " " + address,Addr: nil,Err: err,}}
以上這段代碼的核心在于通過建立好的TCP 鏈接,發送 HTTP 請求調用遠程的HTTP JSON RPC 服務,這里使用的是 HTTP GET 方法。
三、Go 語言的發展前景
Go 語言就是為云而生的編程語言,所以在云原生的時代,它就具備了天生的優勢:易于學習、天然的并發、高效的網絡支持、跨平臺的二進制文件編譯等。
CNCF(云原生計算基金會)對云原生的定義是:
- 應用容器化;
- 面向微服務架構;
- 應用支持容器的編排調度。
對于這三點有代表性的 Docker、K8s 以及 istio 都是采用 Go 語言編寫的,所以 Go 語言在云原生中發揮了極大的優勢。