The PyMiers

Cứ đi là đến (go)


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 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 is simple, but not easy.

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...

Amee

Like

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

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