Skip to content

Commit

Permalink
update part 4.1
Browse files Browse the repository at this point in the history
  • Loading branch information
thoainguyen committed Aug 14, 2019
1 parent 3026b46 commit d05fad5
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 118 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
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
156 changes: 41 additions & 115 deletions ch4-web/ch4-01-introduction.md
Original file line number Diff line number Diff line change
@@ -1,153 +1,79 @@
# 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.

```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])
}
## 4.1.2 Dùng thư viện bên ngoài

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)
}
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`.

// If we fell through, return a 404
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:

```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),..)

0 comments on commit d05fad5

Please sign in to comment.