Skip to content

Commit

Permalink
Merge branch '#issue42-refactor-part(4.1->4.3-4.7->4.10)' into 'dev'
Browse files Browse the repository at this point in the history
Merge 4.1->4.3

See merge request zalopay-oss/advanced-go-book!83
  • Loading branch information
phamtai97 committed Aug 15, 2019
2 parents 3226467 + 843f438 commit eadc35f
Show file tree
Hide file tree
Showing 17 changed files with 212 additions and 536 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.idea
.vscode
.txt
.txt
_book
2 changes: 1 addition & 1 deletion ch1-basic/ch1-05-mem.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 1.5. Concurrent-oriented memory model
# 1.5. Mô hình thực thi tuần tự

Thời gian đầu, CPU chỉ có một lõi duy nhất, các ngôn ngữ khi đó sẽ theo mô hình lập trình tuần tự, điển hình là ngôn ngữ C. Ngày nay, với sự phát triển của công nghệ đa xử lý, để tận dụng tối đa sức mạnh của CPU, mô hình lập trình song song hay [multi-threading](https://en.wikipedia.org/wiki/Multithreading_(computer_architecture)) thường thấy trên các ngôn ngữ lập trình ra đời. Ngôn ngữ Go cũng phát triển mô hình lập trình song song rất hiệu quả với khái niệm Goroutines.

Expand Down
2 changes: 1 addition & 1 deletion ch1-basic/ch1-06-common-concurrency-mode.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 1.6. Concurrency Mode
# 1.6. Mô hình thực thi đồng thời

Một điểm mạnh của Golang là tích hợp sẵn cơ chế xử lý đồng thời (concurrency). Lý thuyết về hệ thống tương tranh của Go là CSP (Communicating Sequential Process) được đề xuất bởi Hoare vào năm 1978. CSP được áp dụng lần đầu cho máy tính đa dụng T9000 mà Hoare có tham gia. Từ NewSqueak, Alef, Limbo đến Golang hiện tại, Rob Pike, người có hơn 20 năm kinh nghiệm thực tế với CSP, rất quan tâm đến tiềm năng áp dụng CSP vào ngôn ngữ lập trình đa dụng. Khái niệm cốt lõi của lý thuyết CSP cũng là của lập trình concurrency trong Go: giao tiếp đồng bộ (synchronous communication). Chủ đề về giao tiếp đồng bộ đã được đề cập trong phần trước. Trong phần này chúng ta sẽ tìm hiểu sơ lược về các mẫu concurrency phổ biến trong Golang.

Expand Down
2 changes: 1 addition & 1 deletion ch1-basic/ch1-07-error-and-panic.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ Do đó, khi đối mặt với giá trị error được return về, giá tr

Ngôn ngữ Go sẽ có một kiểu dữ liệu mạnh, và cụ thể chuyển đổi sẽ được thực hiện giữa những kiểu khác nhau (và sẽ phải bên dưới cùng kiểu dữ liệu). Tuy nhiên, `interface` là một ngoại lệ của ngôn ngữ Go: non-interface kiểu đến kiểu interface, hoặc chuyển đổi từ interface type là cụ thể. Nó cũng sẽ hỗ trợ ducktype, dĩ nhiên, chúng sẽ thỏa mãn cấp độ 3 về bảo mật.

## 1.7.5. Parsing Exception
## 1.7.5. Phân tích ngoại lệ

`Panic` là một hàm dựng sẵn được dùng để dừng luồng thực thi thông thường và bắt đầu `panicking`. Khi hàm `F` gọi `panic`, hàm F sẽ dừng thực thi, bất cứ hàm liên quan tới F sẽ thực thi một cách bình thường, và sau đó lệnh return F sẽ được gọi.

Expand Down
5 changes: 3 additions & 2 deletions ch2-cgo/ch2-05-internal-mechanisms.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ Trong file nguồn Go, nếu một lệnh `import "C"` thực thi thì lệnh cg
<span align="center">
<i>Các file trung gian được CGO tạo ra</i>
</span>
<br/>
</div>
<br/>

Có 4 file Go trong package, trong đó các file nocgo chứa `import "C"` và hai file còn lại chứa code cgo. Lệnh cgo tạo ra hai file trung gian cho mỗi file chứa mã cgo. Ví dụ: main.go tạo ra hai file trung gian là `main.cgo1.go``main.cgo2.c`.

Expand Down Expand Up @@ -145,8 +145,9 @@ Toàn bộ biểu đồ luồng hoạt động của cuộc gọi `C.sum` như s
<span align="center">
<i>Gọi hàm C</i>
</span>
<br/>
</div>
<br/>


Trong đó hàm `runtime.cgocall` là chìa khóa để thực hiện cuộc gọi vượt ranh giới của hàm ngôn ngữ Go sang hàm ngôn ngữ C. Thông tin chi tiết có thể tham khảo <https://golang.org/src/cmd/cgo/doc.go>.

Expand Down
3 changes: 2 additions & 1 deletion ch3-rpc/ch3-06-grpc-ext.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ func (this *Message) Validate() error {
Thông qua hàm Validate() được sinh ra, chúng có thể được kết hợp với `gRPC interceptor`, chúng ta có thể dễ dàng validate giá trị của tham số đầu vào và kết quả trả về của mỗi hàm.

## 3.6.2 REST interface

Hiện nay RESTful JSON API vẫn là sự lựa chọn hàng đầu cho các ứng dụng web hay mobile. Vì tính tiện lợi và dễ dùng của RESTful API nên chúng ta vẫn sử dụng nó để frondend có thể giao tiếp với hệ thống backend. Nhưng khi chúng ta sử dụng framework gRPC của Google để xây dựng các service. Các service sử dụng gRPC thì dễ dàng trao đổi dữ liệu với nhau dựa trên giao thức HTTP/2 và protobuf, nhưng ở phía frontend lại sử dụng [RESTful API](https://restfulapi.net/) API hoạt động trên giao thức HTTP/1. Vấn đề đặt ra là chúng ta cần phải chuyển đổi các yêu cầu RESTful API thành các yêu cầu gRPC để hệ thống các service gRPC có thể hiểu được.

Cộng đồng opensource đã hiện thực một project với tên gọi là [grpc-gateway](https://github.com/grpc-ecosystem/grpc-gateway), nó sẽ sinh ra một proxy có vai trò chuyển các yêu cầu REST HTTP thành các yêu cầu gRPC HTTP2.
Expand Down Expand Up @@ -354,7 +355,7 @@ $ protoc -I. \

File `hello.swagger.json` sẽ được sinh ra sau đó. Trong trường hợp này, chúng ta có thể dùng `swagger-ui project` để cung cấp tài liệu `REST interface` và testing dưới dạng web pages.

## 3.6.3 Docker grpc-gateway
## 3.6.3 Dùng Docker grpc-gateway
Với những lập trình viên phát triển gRPC Services trên các ngôn ngữ không phải Golang như Java, C++, ... có nhu cầu sinh ra grpc gateway cho các services của họ nhưng gặp khá nhiều khó khăn từ việc cài đặt môi trường Golang, protobuf, các lệnh generate,v,v.. Có một giải pháp đơn giản hơn đó là sử dụng Docker để xây dựng grpc-gateway theo bài hướng dẫn chi tiết sau [buildingdocker-grpc-gateway](https://medium.com/zalopay-engineering/buildingdocker-grpc-gateway-e2efbdcfe5c).

## 3.6.4 Nginx
Expand Down
2 changes: 1 addition & 1 deletion ch3-rpc/ch3-07-pbgo.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 3.7 pbgo : Protobuf-based framework
# 3.7 Framework dựa trên Protobuf: pbgo

[Pbgo](https://github.com/chai2010/pbgo) là một framework nhỏ gọn dựa trên cú pháp mở rộng của Protobuf để sinh ra mã nguồn `REST` cho RPC service, trong phần này, chúng ta sẽ cùng tìm hiểu Pbgo.

Expand Down
2 changes: 1 addition & 1 deletion ch3-rpc/ch3-08-grpc-curl.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# 3.8 grpcurl tool
# 3.8 Công cụ grpcurl

Bản thân Protobuf đã có chức năng phản chiếu (reflection) lại file Proto của đối tượng khi thực thi. gRPC cũng cung cấp một package reflection để thực hiện các truy vấn cho gRPC service. Mặc dù gRPC có một hiện thực bằng C++ của công cụ `grpc_cli`, có thể được sử dụng để truy vấn danh sách gRPC hoặc gọi phương thức gRPC, nhưng bởi vì phiên bản đó cài đặt khá phức tạp nên ở đây chúng ta sẽ dùng công cụ `grpcurl` được hiện thực thuần bằng Golang. Phần này ta sẽ cùng tìm hiểu cách sử dụng công cụ này.

Expand Down
155 changes: 40 additions & 115 deletions ch4-web/ch4-01-introduction.md
Original file line number Diff line number Diff line change
@@ -1,153 +1,78 @@
# 4.1 Giới thiệu về Web Development
# 4.1. Giới thiệu về Web Development

Bởi vì gói thư viện `net/http` của Golang chỉ hỗ trợ những hàm routing và hàm chức năng cơ bản. Cho nên trong cộng đồng Golang có một ý tưởng phổ biến là viết thêm các API hỗ trợ routing khác ngoài `net/http`. Theo ý kiến của chúng tôi, nếu các project routing của bạn có những đặc điểm sau: URI cố định, và tham số không truyền thông qua URI, thì nên dùng thư viện chuẩn là đủ. Nhưng với những ngữ cảnh phức tạp hơn, thư viện chuẩn `http` vẫn còn một vài điểm yếu. Ví dụ, xét các route sau:
Phần này sẽ đề cập về cách xây dựng một chương trình web đơn giản bằng thư viện chuẩn của Go, sau đó giới thiệu các framework web trong cộng đồng Open-source.

```
GET /card/:id
POST /card/:id
DELETE /card/:id
GET /card/:id/name
...
GET /card/:id/relations
```

Có thể thấy rằng, cũng là đường dẫn có chứa `/card/:id`, nhưng có phương thức khác nhau và nhánh con khác nhau sẽ dẫn đến cách xử lý khác nhau, logic xử lý những đường dẫn trùng tên như vậy thường sẽ phức tạp.

Framework web của Go có thể được chia thành hai thể loại như sau:
## 4.1.1 Dùng thư viện chuẩn net/http

1. Router framework
2. MVC class framework
Gói thư viện [net/http](https://golang.org/pkg/net/http/) đã cung cấp những hàm cơ bản cho việc routing URL, chúng ta sẽ dùng nó để viết một chương trình `http echo server`:

Khi chọn một framework, trong nhiều trường hợp chúng ta sẽ tham khảo những công nghệ mà công ty đang sử dụng. Ví dụ, nếu công ty có nhiều người làm về `PHP`, thì chúng ta nên chọn framework `beego`, nhưng nếu công ty có nhiều lập trình viên `C`, thì hầu hết những suy nghĩ của họ sẽ đơn giản hết sức có thể. Ví dụ, nhiều lập trình viên C trong những công ty lớn sẽ dùng ngôn ngữ C để viết một chương trình `CGI` nhỏ. Họ không thể sẵn sàng để học `MVC` hoặc nhiều framework Web phức tạp khác. Tất cả những gì họ cần là một route đơn giản, mặc dù họ có thể tự xử lý được nhưng chỉ cần một thư viện xử lý giao thức HTTP cơ bản để giúp họ tiết kiệm công sức làm việc thủ công.

Gói thư viện `net/http` đã cung cấp những hàm chức năng cơ bản, và viết một `http echo server` chỉ mất khoảng 30 giây.
***echo.go:***

```go
//brief_intro/echo.go
package main
import (...)

// các gói thư viện cần import
import (
"io/ioutil"
"log"
"net/http"
)
// hàm routing echo, gồm hai params
// r *http.Request : dùng để đọc yêu cầu từ client
// wr http.ResponseWriter : dùng để ghi phản hồi về client
func echo(wr http.ResponseWriter, r *http.Request) {
// đọc thông điệp mà client gửi tới trong r.Body
msg, err := ioutil.ReadAll(r.Body)
// phản hồi về client lỗi nếu có
if err != nil {
wr.Write([]byte("echo error"))
return
}

// phản hồi về client chính thông điệp mà client gửi
writeLen, err := wr.Write(msg)
// nếu lỗi xảy ra, hoặc kích thước thông điệp phản hồi khác
// kích thước thông điệp nhận được
if err != nil || writeLen != len(msg) {
log.Println(err, "write len:", writeLen)
}
}

// hàm main của chương trình
func main() {
// mapping url ứng với hàm routing echo
http.HandleFunc("/", echo)
// địa chỉ http://127.0.0.1:8080/
err := http.ListenAndServe(":8080", nil)
// log ra lỗi nếu bị trùng port
if err != nil {
log.Fatal(err)
}
}
```

Nếu bạn không thể hoàn thành chương trình trên trong vòng 30 giây, hãy kiểm tra việc bạn gõ phím quá chậm. Đó là ví dụ để minh họa viết một chương trình định tuyến HTTP trong Go sẽ đơn giản như thế nào. Nếu bạn bắt gặp một trường hợp phức tạp hơn, như là một ứng dụng doanh nghiệp cần quá nhiều interfaces, `net/http` sẽ không phù hợp nếu dùng trực tiếp.

Hãy nhìn một dự án Kafka monitoring trong cộng đồng opensource

```go
//Burrow: http_server.go
func NewHttpServer(app *ApplicationContext) (*HttpServer, error) {
...
server.mux.HandleFunc("/", handleDefault)

server.mux.HandleFunc("/burrow/admin", handleAdmin)

server.mux.Handle("/v2/kafka", appHandler{server.app, handleClusterList})
server.mux.Handle("/v2/kafka/", appHandler{server.app, handleKafka})
server.mux.Handle("/v2/zookeeper", appHandler{server.app, handleClusterList})
...
}
```

Hãy đào sâu mã nguồn trên, dự án Kafka monitoring của một công ty nổi tiếng linkedin. Nếu chúng không dùng bất cứ route framework nào và chỉ dùng `net/http`. Nhìn lại mã nguồn trên dường như chúng rất đẹp, chỉ có 5 URIs đơn giản trong dự án của chúng ta, do đó service chúng ta hỗ trợ như sau:
Kết quả khi chạy chương trình:

```sh
/
/burrow/admin
/v2/kafka
/v2/kafka/
/v2/zookeeper
$ go run echo.go &
$ curl http://127.0.0.1:8080/ -d '"Hello, World"'
"Hello, World"
```

Nếu bạn thực sự nghĩ vậy, bạn đã bị lừa. Hãy xem trong hàm `handleKafka()` được định nghĩa như thế nào.
## 4.1.2 Dùng thư viện bên ngoài

```go
func handleKafka(app *ApplicationContext, w http.ResponseWriter, r *http.Request) (int, string) {
pathParts := strings.Split(r.URL.Path[1:], "/")
if _, ok := app.Config.Kafka[pathParts[2]]; !ok {
return makeErrorResponse(http.StatusNotFound, "cluster not found", w, r)
}
if pathParts[2] == "" {
// Allow a trailing / on requests
return handleClusterList(app, w, r)
}
if (len(pathParts) == 3) || (pathParts[3] == "") {
return handleClusterDetail(app, w, r, pathParts[2])
}
Bởi vì gói thư viện chuẩn [net/http](https://golang.org/pkg/net/http/) của Golang chỉ hỗ trợ những hàm routing và hàm chức năng cơ bản. Cho nên trong cộng đồng Golang có một ý tưởng phổ biến là viết thêm các thư viện hỗ trợ routing khác ngoài `net/http`.

switch pathParts[3] {
case "consumer":
switch {
case r.Method == "DELETE":
switch {
case (len(pathParts) == 5) || (pathParts[5] == ""):
return handleConsumerDrop(app, w, r, pathParts[2], pathParts[4])
default:
return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
}
case r.Method == "GET":
switch {
case (len(pathParts) == 4) || (pathParts[4] == ""):
return handleConsumerList(app, w, r, pathParts[2])
case (len(pathParts) == 5) || (pathParts[5] == ""):
// Consumer detail - list of consumer streams/hosts? Can be config info later
return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
case pathParts[5] == "topic":
switch {
case (len(pathParts) == 6) || (pathParts[6] == ""):
return handleConsumerTopicList(app, w, r, pathParts[2], pathParts[4])
case (len(pathParts) == 7) || (pathParts[7] == ""):
return handleConsumerTopicDetail(app, w, r, pathParts[2], pathParts[4], pathParts[6])
}
case pathParts[5] == "status":
return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], false)
case pathParts[5] == "lag":
return handleConsumerStatus(app, w, r, pathParts[2], pathParts[4], true)
}
default:
return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
}
case "topic":
switch {
case r.Method != "GET":
return makeErrorResponse(http.StatusMethodNotAllowed, "request method not supported", w, r)
case (len(pathParts) == 4) || (pathParts[4] == ""):
return handleBrokerTopicList(app, w, r, pathParts[2])
case (len(pathParts) == 5) || (pathParts[5] == ""):
return handleBrokerTopicDetail(app, w, r, pathParts[2], pathParts[4])
}
case "offsets":
// Reserving this endpoint to implement later
return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
}
Thông thường, nếu các dự án routing HTTP của bạn có những đặc điểm sau: URI cố định, và tham số không truyền thông qua URI, thì nên dùng thư viện chuẩn là đủ. Nhưng với những trường hợp phức tạp hơn, thư viện chuẩn `net/http` vẫn còn thiếu các chức năng hỗ trợ. Ví dụ, xét các route sau:

// If we fell through, return a 404
return makeErrorResponse(http.StatusNotFound, "unknown API call", w, r)
}
```sh
GET /card/:id
POST /card/:id
DELETE /card/:id
GET /card/:id/name
GET /card/:id/relations
```

Bởi vì mặc định gói thư viện `net/http` hỗ trợ `mux` routing nhưng không hỗ trợ `arguments`, do đó mã nguồn trên dùng những kĩ thuật rất nhảm nhí như `Split` và bừa bộn như `switch case` để đạt được mục tiêu, điều đó thực sự làm chúng ta tập trung nhiều thời gian vào việc xử lý logic routing hơn là logic business. Nhìn qua hệ thống, thật khó để bảo trì và quản lý. Nếu bạn đọc mã nguồn cẩn thận, bạn sẽ thấy hàm phức tạp nhất là `handleKafka()`. Nhưng thực tế, hệ thống của chúng ta luôn luôn tập hợp nhiều những hàm gây phức tạp như vậy, và cuối cùng sẽ rất khó để làm sạch chúng.
Có thể thấy rằng, cùng là đường dẫn có chứa `/card/:id`, nhưng có phương thức khác nhau hoặc nhánh con khác nhau sẽ dẫn đến logic xử lý khác nhau, cách xử lý những đường dẫn trùng tên như vậy thường sẽ phức tạp. Khi đó chúng ta có thể nghĩ đến việc sử dụng một số framework routing bên ngoài từ cộng đồng Open-sourcce.

Về kinh nghiệm của tôi rất đơn giản, những route một số `parameters` và số lượng APIs cho dự án đó vượt quá 10, thì đừng dùng `net/http` là thư viện route mặc định. Thư viện route được dùng rộng rãi nhất trong cộng đồng opensource Go là `httpRouter`, và nhiều opensource framework router khác cũng dựa trên httpRouter. Nguyên tắc của httpRouter được giải thích chi tiết ở phần router của chương này.
Framework web của Go thể được chia thành hai loại sau:

Nhìn lại phần đầu bài viết, có một vài frameworks trong thế giới opensource. Đầu tiên là một `wrap httpRouter` đơn giản có hỗ trợ để custom middleware và tích hợp một số tiện ích đơn giản như `gin`, nó nhẹ, dễ học, và hiệu nâng cao. Thứ hai là học mô hình MVC của các framework từ các ngôn ngữ lập trình khác.
Thêm vào đó là những nguyên tắc về router và middleware, nội dung của chương này phần lớn là những ví dụ cụ thể bằng mã nguồn Go.
1. Router framework ([HttpRouter](https://github.com/julienschmidt/httprouter), [Gin](https://github.com/gin-gonic/gin), [Gorilla](https://github.com/gorilla/mux),..)
2. MVC class framework ([Revel](https://github.com/revel/revel), [Beego](https://github.com/astaxie/beego), [Iris](https://github.com/kataras/iris),..)
Loading

0 comments on commit eadc35f

Please sign in to comment.