Phần tiếp theo của loạt bài viết Học vừa đủ Golang để nguy hiểm.
Chi tiết về các khái niệm chỉ có trong Go mà không có trong Python như Pointer, sự khác biệt về cách tổ chức package trong Go, declaration & initialization (khai báo và khởi tạo variable), cùng các standard library quan trọng nhất cho một SysAdmin/DevOps.
Code trong loạt bài này lược bỏ phần package/import/declare function main, người đọc tự thêm vào để chạy.
package main
import "fmt"
func main() {
fmt.Println("pp.pymi.vn")
// WRITE CODE HERE
}
Extended Backus-Naur Form (EBNF)
Cú pháp dùng để mô tả Go syntax có tên EBNF.
Có vẻ không dễ đọc, nhưng không phải là không thể đọc nổi, bỏ qua nếu bạn không quan tâm. Một ví dụ:
Production = production_name "=" [ Expression ] "." .
Expression = Alternative { "|" Alternative } .
Alternative = Term { Term } .
Term = production_name | token [ "…" token ] | Group | Option | Repetition .
Group = "(" Expression ")" .
Option = "[" Expression "]" .
Repetition = "{" Expression "}" .
Python 3.9 dùng EBNF kết hợp với PEG
Khai báo và khởi tạo
Declaration & initialization
VarDecl = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .
VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .
Để sử dụng 1 biến (variable) trong Go, cần làm 2 bước declaration (khai báo) và initialization (khởi tạo giá trị).
var x int
var (
y bool
z float64
)
println(x, y, z)
// Output
// 0 false +0.000000e+000
Khai báo biến sử dụng từ khóa var
, nếu không gán giá trị khởi tạo
(initialization), các biến sẽ có zero
value tương ứng với kiểu
của nó. Tức int sẽ = 0, bool = false, string = "", pointer = nil. Nếu có gán
giá trị, biểu thức theo sau sẽ được tính toán và biến sẽ chứa giá trị này.
var y int = 1
var x int = y + 1
print(x)
// Output
// 2
Short variable declarations, không cần ghi type, và dùng dấu :
ShortVarDecl = IdentifierList ":=" ExpressionList .
s := "Pymier"
x, y := 5, 7
x, z := 6, 9
print(s, x, y, z)
// Pymier679
https://golang.org/ref/spec#Variable_declarations
Scope
Scope là phạm vi hoạt động của một biến.
Mỗi tên biến chỉ có thể được định nghĩa duy nhất 1 lần trong mỗi block
(đánh dấu bởi {}
) như trong if/for/switch
hay function, biến này không thoát ra ngoài block - hay có scope trong block đó.
var x int = 3
{
var x string = "Python"
println(x)
}
println(x)
//Python
//3
điều này nghĩa là nếu khai báo 1 variable trong vòng for
hay trong
điều kiện if
, thì chúng không thoát ra ngoài khỏi các khối ấy, sẽ
không có hiện lượng leak variable như trong Python for:
p = "Mi"
s = 0
for p in range(3):
s = s + p
print(p)
# Kết quả hiện ra 2, không hiện ra Mi
print("{} is hoc sinh gioi".format(p))
Tính năng này dễ dẫn đến bug nếu ta chuyển code đi xung quanh, hay vô tình đặt tên giống nhau tại các nơi khác, trong cùng 1 function.
Trong Go:
var p string = "Mi"
s := 0
for p := 0; p < 3; p++ {
s = s + p
}
println(s, p)
// Output
// 3 Mi
https://golang.org/ref/spec#Declarations_and_scope
Struct
struct là khác biệt lớn đầu tiên với người code Python không dùng class (mà dùng dict). Do trong Go không thể tạo 1 map chứa các value khác kiểu, không có đoạn code tương tự với code Python sau đây nếu chỉ dùng map:
boy = {"name": "Pika", "age": 18}
students = {
20088888: boy,
20089999: {"name": "Doraemon", "age": 20},
}
print(students[20088888]["name"])
# Pika
Vậy nên khi cần biểu diễn 1 object trong Go, cần tạo struct:
type Student struct {
name string
age int
}
boy := Student{name: "Pika", age: 18}
fmt.Printf("%v\n", boy)
// {Pika 18}
students := map[int]Student{
20088888: boy,
20089999: Student{name: "Doraemon", age: 20},
}
fmt.Println(students[20088888].name)
// Pika
Và vì thế, map trong Go sẽ chỉ còn được dùng khi cần "ghép cặp" key-value, tối ưu cho việc tìm kiếm theo key. BIG NOTE: Go không có kiểu set built-in.
Để viết code Python "đúng" nhất theo Go, hãy tạo class mỗi khi cần 1 object,
việc này hợp lý trên lý thuyết, nhưng trong thực hành, khi nhận được 1 JSON
string rồi json.loads
lên, không mấy ai ngồi viết class cả.
Trong Go thì bắt buộc phải ngồi viết struct cho JSON đó, hoặc dùng thư viện
cung cấp sẵn struct tương ứng giá trị JSON nói trên nếu dịch vụ có Go client
SDK.
Pointer
Now is the famous POINTER!
pointer vốn làm rụng tóc hàng ngàn sinh viên đại học khi học C, pointer trong Go không khác nhiều với pointer trong C, nhưng sẽ là thứ rất khác biệt với người đến từ Python.
Code trước, nói sau:
x := 96
p := &x
fmt.Printf("Value of p: %v - Type of p: %T\n", p, p)
// Value of p: 0xc000014110 - Type of p: *int
fmt.Println("0xff == 255: ", 0xff == 255)
// 0xff == 255: true
p
có kiểu *int
, và point to x
(trỏ tới x
), dấu *
ở đây là
ký hiệu của kiểu pointer, đứng trước kiểu mà nó trỏ tới.
p
là một pointer trỏ tới x
, x
có kiểu int
nên p
có kiểu *int
.
Còn giá trị của p
là gì? 0xc000014110
là cách viết hệ hexadecimal (hệ 16),
thường được dùng tới khi viết địa chỉ bộ nhớ máy tính.
Trong Python cũng vậy, khi print 1 instance của class (object):
$ python3 -c 'print(object())'
<object object at 0x7f2025653c80>
Chú ý kết quả trên máy bạn sẽ khác, do mỗi lần chạy, Go sẽ cấp 1 memory address khác nhau.
Địa chỉ bộ nhớ (memory address)
Còn có các tên gọi khác như memory location, address.
Trong máy tính, bộ nhớ (RAM) lưu giữ các giá trị của chương trình
đang chạy, các thanh RAM có nhiều "ô", và mỗi ô được đánh số
(trên thực tế phưc tạp hơn khi phân thành 2 loại physical address/logical
address nhưng không bàn tới ở đây).
Khi tạo ra 1 giá trị trong Go, giá trị đó được chứa trong 1 ô, sử dụng phép
toán &
(address operator) sẽ
lấy được địa chỉ này là một số int > 0 biểu diễn ở dạng hex.
Pointer là gì
Một pointer là một memory address - chỉ có vậy.
Nói p
là một pointer point đến x
là cách nói thuật ngữ của câu
p
là giá trị memory address của ô chứa x
.
Trong Go, một pointer luôn phải trỏ tới 1 giá trị x nào đó, ngoại trừ pointer mới declare sẽ có giá trị nil
.
Nếu 1 tấm biển có ghi địa chỉ của 1 quán ăn, thì tấm biển chính là pointer tới quán ăn đó.
Để lấy giá trị mà p
trỏ tới - tức là x
, dùng ký hiệu: *p
.
Tránh nhầm lẫn dấu *
ở đây với dấu *
trong kiểu của pointer. Dấu *
ở đây gọi là pointer indirection
.
x := 96
p := &x
fmt.Printf("Value of *p: %v\n", *p)
*p = 99
fmt.Printf("Value of x: %v\n", x)
// Value of *p: 96
// Value of x: 99
Thay đổi *p
chính là thay đổi x
.
Pointer dùng để làm gì?
Vì pointer là memory address, tức một con số kiểu int
, nó rất nhỏ, nhẹ. Người
ta dùng nó vì muốn "nhẹ", muốn tiết kiệm bộ nhớ (RAM).
Pointer thường được dùng để pass argument cho function hay return kết quả từ
function.
Call by value là gì
Go function call by value, tức nếu gọi function với argument nào, thì function được gọi sẽ nhận được 1 bản copy của argument đó. Khi function return, giá trị return này được copy tới function gọi.
func main() {
n := "meo Meo"
s := strings.ToUpper(n)
...
}
Ví dụ trên, main
gọi ToUpper
, ToUpper
sẽ nhận được 1 bản copy của n
, khi ToUpper
xử lý xong, main
sẽ nhận được 1 bản copy của kết quả mà ToUpper
return.
Copy không có vấn đề gì khi kích thước nhỏ, nhưng khi kích thước dữ liệu lớn, copy sẽ tốn RAM/CPU, vậy nên người ta dùng pointer để chia sẻ chung address, dùng chung 1 giá trị thay vì copy để tiết kiệm RAM. Việc này thực hiện bằng cách gọi function với pointer argument và return kiểu pointer. Khi call function với pointer argument, pointer argument vẫn được copy, nhưng copy address chỉ là copy 1 số int nhỏ bé.
Note: Python call by object reference
Dẫn tới câu hỏi tiếp theo...
Bao nhiêu là lớn, khi nào thì dùng pointer?
Một giá trị kiểu int
với kích thước lớn nhất trong Go là int64
có kích thước 64 bits hay 64/8 == 8 bytes, đây được coi là nhỏ. Vậy bao nhiêu là lớn? 80 bytes? 800 bytes? 8 Kilobyte (8000B, 8KB) ...? quyết định sao là ở lập trình viên, và vì lập trình viên nhiều khi cũng không biết thế nào là lớn, nên hầu hết đều mặc định dùng pointer cho "tiết kiệm RAM"/nhẹ.
Nếu tác giả của Go cũng nghĩ vậy, có lẽ họ mặc định function call by reference (dùng chung thông qua chia sẻ địa chỉ) thay vì call by value (copy giá trị) luôn cho khỏi phải nghĩ.
Nhìn kỹ ra, lý do dùng pointer nói trên là 1 cách để "tối ưu" về bộ nhớ và tốc độ, nhưng việc này lại không tối ưu cho sự đơn giản, dễ hiểu của code - thứ luôn được đánh giá quan trọng hơn trong các chương trình thực tế.
Không bao giờ bạn thấy function sump
nào trông thế này cả, kể cả nó
có vẻ như dùng ít tài nguyên hơn:
func sump(a, b *int) *int {
r := (*a + *b)
return &r
}
func sum(a, b int) int {
return a + b
}
func main() {
a, b := 1, 2
fmt.Printf("%d\n", *sump(&a, &b))
fmt.Printf("%d\n", sum(a, b))
}
// 3
// 3
Nếu tới đây vẫn chưa hiểu pointer là gì, hãy thử vận may với bài viết của Dave Cheney
Method
method của struct là một cú pháp rất sáng tạo và khác biệt với các ngôn ngữ phổ biến (C, Python)... nhưng nó chỉ là 1 cách viết đơn giản (syntactic sugar) để pass struct argument cho method. Ví dụ sau đây, method addAge và function addAge là như nhau.
type User struct {
name string
age int
}
func (u *User) addAge(n int) {
u.age += n
}
func addAge(u *User, n int) {
u.age += n
}
func main() {
u := User{name: "Pika", age: 18}
u.addAge(5)
v := User{name: "Pika", age: 18}
addAge(&v, 5)
fmt.Printf("%v == %v\n", u, v)
}
// Kết quả như nhau
struct
argument được mặc định pass vào cho method gọi là method receiver
,
method receiver hầu hết đều là pointer, lý do xem lại phần pointer và link đính kèm.
method receiver
giống như self
argument trong method của Python class.
Interface
Một var kiểu interface{}
có thể chứa mọi giá trị, biến Go thành dynamic
typing giống như Python, nhưng đấy không phải lý do người ta tạo ra interface.
Trích Go spec:
An interface type specifies a method set called its interface. A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface. The value of an uninitialized variable of interface type is nil.
InterfaceType = "interface" "{" { ( MethodSpec | InterfaceTypeName ) ";" } "}" .
MethodSpec = MethodName Signature .
MethodName = identifier .
InterfaceTypeName = TypeName .
kiểu interface
dùng để định nghĩa 1 bộ các method. Thay vì cách dùng trong OOP phổ biến (Java, C#), một giá trị kiểu A là một giá trị kiểu A hoặc kế thừa kiểu A (inheritance), thì Go nói một giá trị kiểu A nếu nó có đủ các method trong interface A.
Nhờ tính năng này, Go có thể viết 1 function nhận vào 1 interface, và khi gọi, có thể nhận vào bất kỳ kiểu nào đã implement interface này. Đây gọi là polymorphism
- 1 trong 4 đặc tính quan trọng của OOP.
Packages
Mỗi thư mục trong Go là 1 package riêng biệt, thư mục con là 1 package khác, không liên quan gì tới thư mục chứa nó.
Thư mục internal/
chỉ cho phép các thư mục chứa nó dùng, không cho phép các thư mục/project khác dùng.
Concurrency
Concurrency trong Go là một vấn đề phức tạp.
Mặc dù concurrency là một thế mạnh của Go so với các ngôn ngữ khác,
nó vẫn đầy những vấn đề khó khăn, dễ sai, và không hoàn hảo (goroutine leak).
thread & multiprocess & async trong Python, hay các ngôn ngữ khác, cũng không phải ngoại lệ.
Cost
Một triết lý trong thiết kế của Go là khiến lập trình viên nhìn thấy "cost" của code mình viết. Ví dụ trong Python viết:
1 in [4,5,6] # O(n)
1 in {5: 'a', 6: 'b'} # O(1)
chỉ thấy 1 chữ in
và không thấy rõ là phải mất bao nhiêu phép tính, thì Go tối ưu thiết kế cho việc đó, dẫn tới việc muốn tìm 1 giá trị trong 1 slice/array, chỉ có cách là dùng 1 vòng for duyệt qua từng phần tử -> O(n).
Stdlib for DevOps
- Xử lý CLI argument:
flag
- Logging:
log
- Testing:
testing
- Xử lý file, thư mục, process:
io
,os
- HTTP:
net/http
- JSON:
encoding/json
Cộng đồng Go ưa chuộng việc copy 1 chút thay vì cài thêm thư viện bên ngoài, ưu chuộng dùng bộ stdlib có sẵn.
Example
Script liệt kê các GitHub repositories của 1 GitHub user (NOTE: no pagination)
/*
Script lists GitHub repos of given username.
*/
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
)
func main() {
urlFmt := "https://api.github.com/users/%s/repos"
flag.Parse()
username := flag.Arg(0)
if len(username) == 0 {
fmt.Printf("Usage: ./cli username\n")
os.Exit(1)
}
url := fmt.Sprintf(urlFmt, username)
resp, err := http.Get(url)
if err != nil {
log.Fatalf("Failed to get URL: %s", err)
}
decoder := json.NewDecoder(resp.Body)
var repos []struct {
Name string
HtmlUrl string `json:"html_url"`
}
err = decoder.Decode(&repos)
if err != nil {
log.Fatalf("Failed to decode JSON: %s", err)
}
for idx, i := range repos {
fmt.Printf("%3d %s %s\n", idx+1, i.Name, i.HtmlUrl)
}
}
Go đơn giản, nhưng không dễ
Go đơn giản - đơn giản tức là ít khái niệm, ít cú pháp. Nhưng không dễ - dễ tức là không mất nhiều suy nghĩ, nhiều công sức làm một việc. Kiểm tra 1 phần tử trong slice, thêm 1 phần tử vào giữa array, ... các "bài toán nhỏ" mà Python chỉ mất 1 dòng code, thì trong Go là cả một vấn đề lớn mà người ta phải viết hẳn wiki rồi chia sẻ.
Go có không it trap/gotchas dù ngôn ngữ có vẻ đơn giản:
Like & Dislike
Vậy anh có thích Go hay không?
Đó là 1 câu hỏi bẫy! ngày còn 20, tôi sẽ trả lời ngay là không hay có, nhưng giờ nhận ra, cuộc sống đâu phải là binary mà chỉ có 0 với 1. Khi ta thích thứ gì, ta có thể thích chỗ này không thích chỗ kia. Khẩu vị con người cũng thay đổi, thứ không thích hôm nay có khi mai lại "từ thích thích thành thương thương"... Ngôn ngữ lập trình cũng chỉ là công cụ, có gì mà phải yêu với ghét...
Like
- Compile thành binary + cross compiling: deploy trở thành câu chuyện rất đơn giản: copy file.
- Bộ stdlib đầy đủ: HTTP, JSON - hai công cụ quan trọng của DevOps.
- Được hỗ trợ rộng rãi bởi các nhà cung cấp dịch vụ: AWS, GCP... tất nhiên Python cũng được hỗ trợ nhiều, nhưng nếu bạn dùng 1 ngôn ngữ trẻ nào đó như... Elixir, Rust hay già mà không rất phổ biến như Erlang, Ocaml thì đều không được hỗ trợ nhiều vậy.
Dislike
- Code dài dòng
- Không easy
- Quản lý package thay đổi liên tục từ 2014, giờ chắc đã ổn.
Kết luận
Go là một ngôn ngữ đơn giản nhưng không dễ, ai cũng học được nhưng để có thể dùng thoải mái sẽ cần một thời gian luyện tập cho quen sau khi đã dính lời nguyền ngắn gọn, dễ dàng của Python. Go là một ngôn ngữ hiện đại, tốt, đáng bỏ thời gian đầu tư làm quen để cho vào bộ công cụ của bạn... và vẫn yêu Python.
References
- Go spec
- go-proverbs
- Ocaml to F#
- Dave Cheney on pointer
- Go easy
- Ultimate Go - non-free course
- Simple made easy
HVN at http://pymi.vn and https://www.familug.org sau một lần nữa 6 tháng code Go và chắc lại còn lâu mới code nữa...
Comments