Category Archives: Go – Lập trình Go

Go – Network

Trong phần này chúng ta sẽ tìm hiểu cách lập trình mạng trong Go.

Hiện tại thì có rất nhiều thư viện do các coder bên ngoài viết hỗ trợ lập trình mạng rất tốt. Tuy nhiên chúng ta sẽ sử dụng thư viện có sẵn của Go là gói net vì thư viện mặc định này cũng khá mạnh mẽ rồi.

TCP Server

Đầu tiên chúng ta sẽ viết một ứng dụng client-server sử dụng giao thức TCP đơn giản. Chúng ta sẽ viết 2 file là server.go và client.go

package main

import (
    "encoding/gob"
    "fmt"
    "net"
)

func server() {
    ln, err := net.Listen("tcp", "127.0.0.1:12345")
    if err != nil {
    fmt.Println(err)
        return
    }
    defer ln.Close()
    fmt.Println("Server starts listening on port 12345")
    for {
        c, err := ln.Accept()
        if err != nil {
            fmt.Println(err)
            continue
        } 
        go handleServerConnection(c)
    }
}

func handleServerConnection(c net.Conn) {
    var msg string
    err := gob.NewDecoder(c).Decode(&msg)
    if err != nil {
       fmt.Println(err)
    } else {
       fmt.Println("Received : ", msg)
    }
    c.Close()
}

func main() {
    server()
}

File server.go sẽ chạy phần server, ở đây chúng ta sử dụng gói net để thực hiện việc lắng nghe các yêu cầu từ client.

ln, err := net.Listen("tcp", "127.0.0.1:12345")
if err != nil {
fmt.Println(err)
    return
}
defer ln.Close()

Để bắt đầu lắng nghe thì chúng ta gọi hàm net.Listen(), hàm này nhận tham số là tên giao thức và địa chỉ cổng lắng nghe, ở đây chúng ta dùng giao thức tcp, cổng lắng nghe là 12345, tất nhiên bạn có thể sử dụng các cổng khác tùy ý, hàm này trả về một biến kiểu net.Listener và một biến error. Câu lệnh defer cuối cùng sẽ đảm bảo khi kết thúc chương trình thì cổng cũng sẽ được trả lại cho hệ điều hành.

for {
    c, err := ln.Accept()
    if err != nil {
        fmt.Println(err)
        continue
    } 
    go handleServerConnection(c)
}

Sau khi lắng nghe thì chúng ta đi vào một vòng lặp vô hạn, mỗi lần lặp chúng ta gọi hàm ln.Accept(), hàm này sẽ kiểm tra xem có ứng dụng client nào kết nối đến server hay không, nếu không có thì đợi đến khi có và chấp nhận kết nối đó rồi trả về một biến kiểu net.Conn và một biến error. Biến net.Conn lưu các thông tin về client đã kết nối tới server. Nếu quá trình chấp nhận kết nối không có lỗi thì chúng ta xử lý kết nối đó trong hàm handleServerConnection() và hàm này được thực hiện trong một routine riêng biệt với routine của hàm main(), nếu bạn chưa biết routine là gì thì xem bài Concurrency, nếu có lỗi thì chúng ta bỏ qua.

func handleServerConnection(c net.Conn) {
    var msg string
    err := gob.NewDecoder(c).Decode(&msg)
    ...
}

Trong hàm handleServerConnection(), chúng ta đọc dữ liệu truyền từ client đến, ở đây client sẽ gửi những chuỗi tin nhắn đã được mã hóa bằng gói gob (phần code client ở dưới), do đó chúng ta giải mã chuỗi đó bằng hàm gob.NewDecoder() rồi dùng hàm Decode() truyền nội dung đã được giải mã đó vào biến msg để in ra.

package main

import (
    "fmt"
    "net"
    "encoding/gob"
)

func client() {
    c, err := net.Dial("tcp", "127.0.0.1:12345")
    if err != nil {
        fmt.Println(err)
        return
    }
 
    msg := "Hello server"
    fmt.Println("Sending : ", msg)
    err = gob.NewEncoder(c).Encode(msg)
    if err != nil {
        fmt.Println(err)
    }
    c.Close()
}

func main() {
    client()
}

Trong file client.go chúng ta thực hiện kết nối và gửi dữ liệu đến server.

c, err := net.Dial("tcp", "127.0.0.1:12345")

Để kết nối đến một server, chúng ta sử dụng hàm net.Dial(), hàm này nhận vào 2 tham số là tên giao thức và địa chỉ IP cùng số cổng. Giá trị trả về là một biến net.Conn và một biến error.

msg := "Hello server"
...
err := gob.NewEncoder(c).Encode(msg)

Để mã hóa và gửi dữ liệu lên server thì ở đây chúng ta dùng hàm gob.NewEncoder().Encode()

go run server.go
Server starts listening on port 12345
Received : Hello server
go run client.go
Sending : Hello server

HTTP Server

Chúng ta còn có thể viết cả một webserver với gói net/http. Ví dụ:

package main

import (
    "net/http"
    "io"
)

func hello(res http.ResponseWriter, req *http.Request) {
    res.Header().Set(
        "Content-Type",
        "text/html",
    )
    content := `<!DOCTYPE html>
                <html>
                    <head>
                        <title>Sample Go Web Server</title>
                    </head>
                    <body>
                        <h1>It Worked!</h1>
                    </body>
                </html>`
    io.WriteString(
        res,
        content,
    )
}

func main() {
    http.HandleFunc("/", hello)
    http.ListenAndServe("127.0.0.1:12345", nil)
}

Trong đoạn code trên, chúng ta viết một webserver.

http.HandleFunc("/", hello)
http.ListenAndServe("127.0.0.1:12345", nil)

Hàm http.HandleFunc() cho biết khi nhập đường dẫn URL đến địa chỉ của server thì trả về nội dung trong hàm hello().

Hàm http.ListenAndServe() cho server chạy trên cổng 12345.

func hello(res http.ResponseWriter, req *http.Request) {
    ...
}

Bất cứ hàm nào làm công việc trả về nội dung HTML cho client đều phải có 2 tham số đầu vào là một biến http.ResponseWriter và một biến *http.Request. Trong đó biến http.ResponseWriter sẽ được dùng để gửi trả dữ liệu về, *http.Request là biến chứa thông tin về client.

res.Header().Set(
    "Content-Type",
    "text/html",
)

Đầu tiên chúng ta cho biết dữ liệu trả về sẽ là nội dung HTML, bằng cách thiết lập trong hàm res.Header().Set()

content := `<!DOCTYPE html>
           ...
            </html>`
io.WriteString(
    res,
    content,
)

Tiếp theo chúng ta ghi nội dung HTML trong biến content (chuỗi được bọc trong cặp dầu huyền `` có thể ghi trên nhiều dòng), cuối cùng để chuyển dữ liệu đó về client thì chúng ta có thể dùng hàm io.WriteString(), hàm này nhận vào đối tượng sẽ được ghi dữ liệu (là res) và dữ liệu sẽ được ghi (là content).

capture

Go – Hàm băm và mã hóa

Hàm băm (tiếng Anh: Hash) là các hàm làm công việc biến một chuỗi giá trị bình thường thành một chuỗi giá trị khác ngắn hơn và có chiều dài cố định, chuỗi này còn gọi là giá trị băm. Giá trị băm sẽ được dùng để tìm kiếm và đánh chỉ mục trong cơ sở dữ liệu, bởi vì khi chúng ta cần tìm một thứ gì đó trong cơ sở dữ liệu thì việc tìm thông qua các giá trị băm sẽ nhanh hơn nhiều so với tìm chuỗi gốc. Ngoài ra hàm băm còn được dùng nhiều trong các thuật toán mã hóa do đó hàm băm còn được chia làm 2 loại là loại có mã hóa và loại không có mã hóa.

Ví dụ chúng ta có cơ sở dữ liệu lưu trữ tên người gồm: Abernathy, Sara Epperdingle, Roscoe Moore, Wilfred Smith, David, khi chúng ta cần tìm tên của ai đó trong cơ sở dữ liệu, như bình thường thì chúng ta sẽ so sánh chuỗi xem từ khóa tìm có giống với tên người đó hay không, nếu giống thì trả về, không thì tiếp tục tìm cho đến hết. Tuy nhiên thay vì lưu trữ tên gốc như thế, chúng ta có thể dùng hàm băm để chuyển các tên người đó thành các giá trị là số có 4 chữ số như Abernathy – 7864, Sara 9802 Epperdingle – 9802, Roscoe Moore – 1990, Wilfred Smith – 8822 , David – 7822 (tất nhiên trong thực tế thì giá trị này không chỉ có 4 chữ số), và khi tìm kiếm thì thay vì tìm kiếm các chuỗi gốc, chúng ta sẽ tìm kiếm bằng cách tính giá trị băm cho từ khóa rồi so sánh với các giá trị băm trong cơ sở dữ liệu.

Hàm băm không mã hóa

Trong Go có sẵn một số gói chứa hàm băm không mã hóa là crc32, adler32, crc64fnv, các gói này đều nằm trong gói hash. Ví dụ về crc32:

package main

import "fmt"
import "hash/crc32"

func main() {
 
    str1 := []byte("There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain") 
    hash1 := crc32.NewIEEE()
    hash1.Write(str1)
    fmt.Println(hash1.Sum32())
 
    str2 := []byte("There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain...")
    hash2 := crc32.NewIEEE()
    hash2.Write(str2)
    fmt.Println(hash2.Sum32())
 
    if hash1.Sum32() != hash2.Sum32() {
        fmt.Println("Different")
    } else {
        fmt.Println("Same")
    }
}

Trong đoạn code trên, chúng ta tính giá trị băm cho 2 chuỗi bằng cách dùng gói hash/crc32 và so sánh xem 2 chuỗi đó có giống nhau hay không (chuỗi thứ 2 có dấu 3 chấm ở cuối chuỗi).

hash1 := crc32.NewIEEE()

Đầu tiên chúng ta dùng hàm crc32.NewIEEE(), hàm này sẽ tạo một đối tượng kiểu hash.Hash32.

hash1.Write(str1)

Tiếp theo chúng ta gọi hàm Write() để ghi dữ liệu trong biến str1 vào đối tượng Hash32 đó.

fmt.Println(hash1.Sum32())

Cuối cùng hàm Sum32() sẽ tính giá trị băm trả về giá trị đó là một số nguyên 32-bit kiểu uint32.

if hash1.Sum32() != hash2.Sum32() {
    fmt.Println("Different")
} else {
    fmt.Println("Same")
}

Và chúng ta chỉ cần so sánh giá trị uint32 này là có thể biết được 2 chuỗi giống nhau hay khác nhau.

3455434559
2617155743
Different

Nếu bạn muốn biết thuật toán CRC32 hoạt động như thế nào hay muốn tìm hiểu xem tại sao hàm băm này lại có thể tạo ra các chuỗi khác nhau riêng biệt mà không sợ trùng thì tìm hiểu thêm trên internet, ở đây mình không đi sâu.

Hàm băm có mã hóa

Các hàm băm có mã hóa thì cũng giống hàm băm không mã hóa, chỉ khác là chúng không thể bị đảo ngược, tức là chúng ta không có cách nào để chuyển từ một giá trị băm về chuỗi giá trị gốc được. Do đó các hàm này thường dùng trong các ứng dụng bảo mật (thay vì dùng cho tìm kiếm hoặc kiểm tra xem dữ liệu có thay đổi hay không như CRC32 ở trên).

Một số thuật toán băm có mã hóa thông dụng hiện nay là SHA-1, SHA-256, MD5… Ví dụ về sha-1:

package main

import "fmt"
import "crypto/sha1"

func main() {
 
    str1 := []byte("There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain") 
    crypto := sha1.New()
    crypto.Write(str1)
    fmt.Println(crypto.Sum([]byte{}))
    fmt.Printf("%x", crypto.Sum([]byte{}))
}

Gói crypto trong Go chứa gói sha1 dùng trong việc tính giá trị băm theo thuật toán SHA-1.

Cách sử dụng các hàm băm này cũng tương tự như với hàm băm CRC32, chúng ta dùng hàm sha1.New() để tạo một đối tượng hash.Hash. Sau đó dùng hàm Write() để ghi dữ liệu vào đối tượng Hash này, rồi dùng hàm Sum() để tính giá trị băm.

Một điểm khác của hàm băm SHA-1 với CRC32 là SHA-1 sẽ trả về giá trị 160 bit chứ không phải chỉ có 32 bit, và vì trong Go không có kiểu int nào lớn tới 160 bit nên hàm Sum() sẽ trả về một slice chứa 20 phần tử kiểu byte (mỗi byte 8 bit). Ngoài ra hàm Sum() còn nhận một tham số đầu vào là một slice kiểu byte nữa, ý nghĩa của tham số này là giá trị salt trong thuật toán SHA-1. Thường dùng trong xác thực mật khẩu, ở đây chúng ta để trống, nếu muốn bạn có thể tìm hiểu thêm trên internet.

Ở đoạn code trên chúng ta in giá trị băm dưới dạng số nguyên gốc và số hệ 16.

[29 187 202 66 188 186 226 160 230 54 155 114 184 195 234 87 232 6 193 155]
1dbbca42bcbae2a0e6369b72b8c3ea57e806c19b

Go – List

Trong phần này chúng ta sẽ tìm hiểu về kiểu dữ liệu List trong Go.

Ngoài các kiểu dữ liệu dạng danh sách mà chúng ta đã tìm hiểu như array, slice và map, Go còn cung cấp một kiểu dữ liệu khác là List có trong gói container/list.

List

Kiểu List Gói container/list thực chất chính là cấu trúc Danh sách liên kết đôi, nếu bạn đã từng học môn Cấu trúc dữ liệu ở Đại học thì sẽ biết cấu trúc này.

Capture

Danh sách liên kết là một danh sách các biến struct kiểu Node do chúng ta tự định nghĩa, các biến Node này có các trường dữ liệu thông thường (như int, float, string...) và một trường đặc biệt là một con trỏ kiểu Node, con trỏ này lưu địa chỉ của Node tiếp theo trong danh sách.

Danh sách liên kết đôi thì mỗi Node chứa 2 con trỏ, một con trỏ chỉ tới Node phía sau nó và một con trỏ chỉ tới Node phía trước nó.

Danh sách liên kết cho phép chúng ta sử dụng bộ nhớ máy tính một cách linh hoạt hơn.

Trong Go thì chúng ta có thể sử dụng struct List đã được định nghĩa sẵn như sau:

package main

import "fmt"
import "container/list"

func main() {
    var x list.List
 
    x.PushBack(1)
    x.PushBack(2.5)
    x.PushBack("Hello")

    for e := x.Front() ; e != nil ; e = e.Next() {
        fmt.Println(e.Value)
    }
}

Trong đoạn code trên, chúng ta tạo một biến List là x, khi tạo thì x rỗng, không có phần tử nào cả.

x.PushBack(1)
x.PushBack(2.5)
x.PushBack("Hello")

Chúng ta thêm một phần tử vào List bằng hàm PushBack(), chúng ta có thể đưa vào bất cứ kiểu dữ liệu gì cũng được, từ int, float cho tới string... hoặc một struct do chúng ta tự tạo cũng được.

for e := x.Front() ; e != nil ; e = e.Next() 

Để lặp qua một list thì chúng ta có thể dùng vòng lặp for như trong ví dụ, hàm Front() sẽ trả về phần tử đầu tiên của list, hàm Next() sẽ trả về phần tử tiếp theo trong list, chúng ta kiểm tra cho đến khi không còn phần tử nào nữa (nil) thì vòng lặp dừng.

fmt.Println(e.Value)

Để lấy giá trị mà phần tử đang lưu trữ thì chúng ta đọc trường Value là được.

1
2.5
Hello

Sort

Trong Go có gói sort chứa các hàm hỗ trợ sắp xếp các danh sách với các kiểu dữ liệu thường dùng. Ví dụ:

package main

import "fmt"
import "sort"

func main() {

    x := []int{93, 48, 27, 784, 13}
    fmt.Println(x)
    sort.IntSlice.Sort(x)
    fmt.Println(x)
 
    y := []float64{3.5, 7.6, 8.93, 5.23, 7.609}
    fmt.Println(y)
    sort.Float64Slice.Sort(y)
    fmt.Println(y)
}

Trong đoạn code trên chúng ta có 2 slice xy, slice x chứa các phần tử kiểu int, slice y chứa các phần tử kiểu float64.

sort.IntSlice.Sort(x)
...
sort.Float64Slice.Sort(y)

Gói sort có 2 struct là IntSliceFloat64Slice, 2 struct này có hàm Sort()  có chức năng sắp xếp các phần tử trong một slice kiểu int hoặc kiểu float64.

[93 48 27 784 13]
[13 27 48 93 784]
[3.5 7.6 8.93 5.23 7.609]
[3.5 5.23 7.6 7.609 8.93]

Trong trường hợp chúng ta muốn sắp xếp các kiểu dữ liệu “không thường dùng” như struct mà chúng ta tự định nghĩa, thì chúng ta làm như sau:

package main

import "fmt"
import "sort"

type Person struct {
    Name string
    Age int
}

type ByName []Person

func (this ByName) Len() int {
    return len(this)
}
func (this ByName) Less(i, j int) bool {
    return this[i].Name < this[j].Name
}
func (this ByName) Swap(i, j int) {
     this[i], this[j] = this[j], this[i]
}
func main() {
    kids := []Person{
        {"Jill", 9},
        {"Jack", 10},
    }
    sort.Sort(ByName(kids))
    fmt.Println(kids)
}

Để một slice kiểu struct mới có thể sắp xếp được bằng hàm Sort() thì slice đó phải có 3 phương thức là Len(), Less()Swap(). Trong đó phương thức Len() trả vể số lượng phần tử của slice, Less() so sánh 2 phần tử i và j cái nào bé hơn, Swap() đổi chỗ 2 phần tử i và j cho nhau. Chẳng hạn như trong đoạn code trên chúng ta định nghĩa một struct có tên Person và một slice kiểu Person có tên ByName. Struct Person có 2 trường là NameAge, slice ByName có 3 phương thức Len(), Less()Swap(). Hàm Less() so sánh 2 phần tử bé hơn bằng cách so sánh trường Name của mỗi phần tử đó.

[{Jack 10} {Jill 9}]

Nếu muốn sắp xếp theo trường Age thì chúng ta cũng làm tương tự với hàm Less():

func (this ByName) Less(i, j int) bool {
    return this[i].Age < this[j].Age
}

Go – Xử lý lỗi

Trong các bài trước chúng ta đã từng thấy một kiểu biến struct có tên là error. Trong phần này chúng ta sẽ tìm hiểu kỹ hơn.

Nếu bạn đã từng lập trình Java, C#, PHP… thì có lẽ bạn đã quen với khái niệm lỗi ngoại lệ – Exception, lỗi này còn có một tên khác là Runtime Error, nếu chưa thì mình nói luôn là lỗi này là loại lỗi chỉ xuất hiện khi chúng ta chạy chương trình. Ví dụ dễ hiểu nhất khi học về lỗi ngoại lệ là chương trình máy tính bỏ túi, chương trình này rất đơn giản, chỉ là yêu cầu người dùng cung cấp 2 biến số và phép tính, chúng ta tính toán giá trị rồi hiển thị lên cho người dùng, tuy nhiên chương trình này sẽ bị lỗi ngoại lệ ở phép chia có mẫu số là 0. Đây là một lỗi ngoại lệ vì nó chỉ xuất hiện khi người dùng nhập vào số 0 – tức là lúc chương trình đang chạy.

Trong các ngôn ngữ Java, C#, PHP… thì chúng ta xử lý lỗi ngoại lệ bằng cách “bắt” lấy chúng. Còn trong Go thì có hơi khác là gán chúng vào một biến rồi xử lý. Do đó cơ chế này cho phép chúng ta theo dõi lỗi xuất hiện ở dòng code nào dễ hơn.

Tạo biến error

Trong Go thì khi lỗi ngoại lệ xuất hiện, các hàm sẽ trả về một biến kiểu error chứa thông tin về lỗi đó. Ví dụ:

package main

import "errors"
import "fmt"

func divide(arg1, arg2 int) (int, error) {
    if arg2 == 0 {
       return -1, errors.New("Can't divide by 0")
    }
    return arg1 / arg2, nil
}

func main() {
    arg1 := 10
    arg2 := 0
    result, err := divide(arg1, arg2)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
}

Chúng ta dùng hàm errors.New() để tạo một biến kiểu error. Hàm này nhận vào một chuỗi để mô tả lỗi xảy ra.

func divide(arg1, arg2 int) (int, error) {
    if arg2 == 0 {
       return -1, errors.New("Can't divide by 0")
    }
    return arg1 / arg2, nil
}

Trong ví dụ trên, chúng ta viết hàm divide() có chức năng thực hiện phép chia 2 số nguyên, chúng ta kiểm tra nếu mẫu số arg2 là 0 thì trả về -1 và một biến error. Nếu không có lỗi thì trả về kết quả phép chia, còn biến error chúng ta trả về là nil tức là rỗng.

result, err := divide(arg1, arg2)
if err != nil {
    ...
} else {
    ...
}

Khi gọi hàm divide(), chúng ta cũng gán 2 giá trị trả về vào 2 biến resulterr, sau đó chúng ta kiểm tra xem biến err có khác rỗng hay không, nếu khác rỗng thì tức là có lỗi, chúng ta in lỗi đó ra, nếu không rỗng thì chúng ta in kết quả ra.

Can't divide by 0

Tùy chỉnh thông báo lỗi

Nếu như hàm errors.New() chỉ cho phép chúng ta tạo biến error với chuỗi cố định thì chúng ta có thể tùy chỉnh để in chuỗi thông báo lỗi một cách linh hoạt hơn.

error trong Go là một interface có phương thức Error() thực hiện việc in một chuỗi ra màn hình. Chúng ta có thể code lại phương thức đó để tùy chỉnh chuỗi in ra. Ví dụ:

package main

import "fmt"

type fraction struct {
    arg1, arg2 int
}

func (e *fraction) Error() string {
    return fmt.Sprintf("%d can't divide by %d", e.arg1, e.arg2)
}

func divide(arg1, arg2 int) (int, error) {
    if arg2 == 0 {
        err := fraction{arg1, arg2}
        return -1, &err
    }
    return arg1 / arg2, nil
}

func main() {
    arg1 := 10
    arg2 := 0

    result, err := divide(arg1, arg2)
    if err != nil {
        fmt.Println(err)
    } else {
        fmt.Println(result)
    }
}

Ở đây chúng ta định nghĩa một struct có tên fraction để lưu trữ một phân số với tử số là arg1, mẫu số là arg2.

func (e *fraction) Error() string {
    return fmt.Sprintf("%d can't divide by %d", e.arg1, e.arg2)
}

Chúng ta code lại phương thức Error() cho struct fraction, ở đây chúng ta dùng hàm fmt.Sprintf() để tạo một chuỗi tùy ý.

func divide(arg1, arg2 int) (int, error) {
    if arg2 == 0 {
        err := fraction{arg1, arg2}
        return -1, &err
    }
    return arg1 / arg2, nil
}

Trong hàm divide(), thay vì chúng ta tạo một biến error từ hàm errors.New() thì bây giờ chúng ta tạo một biến kiểu fraction rồi trả về biến đó, lưu ý là chúng ta phải trả về địa chỉ đến biến struct đó.

10 can't divide by 0

Mặc dù chúng ta định nghĩa kiểu fraction nhưng khi trả về thì hàm divide() sẽ trả về kiểu error. Trong trường hợp chúng ta muốn lấy kiểu trả về là kiểu gốc là fraction thì chúng ta tiến hành ép kiểu như sau:

result, err := divide(10, 0)
frac, ok := err.(*fraction)
if ok {
    fmt.Println(frac.arg1)    // 10
    fmt.Println(frac.arg2)    // 0
}

Go – Nhập xuất (I/O)

Gói io trong Go chỉ chứa một số ít các hàm, còn lại phần lớn là các interface, trong đó có 2 interface chính là ReaderWriter. Reader chứa các hàm hỗ trợ đọc dữ liệu (nhập), Writer chứa các hàm hỗ trợ ghi dữ liệu (xuất). Hầu hết các hàm trong Go nhận tham số là các biến Reader hoặc Writer này.

Ví dụ gói io có hàm Copy() có chức năng sao chép dữ liệu từ một Reader sang một Writer:

func Copy(dst Writer, src Reader) (written int64, err error)

Để đọc hoặc ghi dữ liệu vào một slice []byte hoặc một string thì chúng ta có thể dùng struct Buffer trong gói bytes:

var buf bytes.Buffer
buf.Write([]byte("test"))

Biến Buffer có thể không cần phải khởi tạo trước, Buffer hỗ trợ cả ReaderWriter. Từ biến Buffer chúng ta có thể chuyển thành một slice []byte bằng cách dùng hàm Bytes(). Nếu chúng ta chỉ có nhu cầu đọc dữ liệu từ string thì có một hàm khác tiện hơn là strings.NewReader().

Go – File và thư mục

Trong phần này chúng ta sẽ học cách đọc và ghi file.

Đọc file

Chúng ta sẽ viết một đoạn code mở file text có nội dung như sau để đọc:

Hello World

Còn đây là đoạn code mở file để đọc:

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("test.txt")
    if err != nil {
        fmt.Println("Error: ", err)
        return
    }
    defer file.Close()
 
    stat, err := file.Stat()
    if err != nil {
        fmt.Println("Error: ", err)
        return
    }
 
    bs := make([]byte, stat.Size())
    _, err = file.Read(bs)
    if err != nil {
        fmt.Println("Error: ", err)
        return
    }
 
    str := string(bs)
    fmt.Println(str)
} 

Để mở file thì chúng ta dùng hàm Open() trong gói os, hàm này nhận vào đường dẫn đến file, giá trị trả về là một biến struct tên là File và một biến struct tên là error, biến File sẽ dùng cho việc đọc/ghi file, biến error cho biết hàm Open() thực thi có thành công hay không.

Lưu ý là đường dẫn file có thể là đường dẫn tuyệt đối hoặc tương đối, nếu bạn chạy chương trình bằng lệnh go run thì phải dùng đường dẫn tuyệt đối nếu không sẽ có lỗi không tìm thấy file, nếu bạn build đoạn code ra file .exe bằng lệnh go install thì có thể để đường dẫn tương đối.

if err != nil {
    fmt.Println("Error: ", err)
    return
}

Do đó sau khi mở file chúng ta kiểm tra xem quá trình mở có bị lỗi hay không bằng cách kiểm tra xem biến error có rỗng hay không, nếu không rỗng thì tức là đã có lỗi và chúng ta cho thoát chương trình luôn bằng cách dùng lệnh return trong hàm main().

defer file.Close()

Câu lệnh defer thực hiện việc đóng file sau khi chương trình trong hàm main() kết thúc, những câu lệnh mang tính chất dọn dẹp tài nguyên như vậy nên được để gần với câu lệnh xin cấp phát tài nguyên để dễ quản lý.

stat, err := file.Stat()
if err != nil {
    fmt.Println("Error: ", err)
    return
}

Tiếp theo chúng ta dùng hàm Stat() của biến file để lấy về một biến struct kiểu FileInfo, biến này lưu những thông tin cơ bản của file như tên file, ngày tạo, kích thước… Hàm Stat() cũng trả về thêm biến error nên chúng ta cũng nên kiểm tra trước khi tiếp tục.

bs := make([]byte, stat.Size())
_, err = file.Read(bs)

Tiếp theo chúng ta tạo một slice kiểu byte có kích thước lấy từ biến FileInfo để lưu từng byte dữ liệu đọc từ file, việc đọc dữ liệu từ file và ghi vào slice được thực hiện thông qua hàm Read() trong biến File, hàm này nhận tham số là slice mà nó sẽ ghi vào. Hàm Read() cũng có trả về một biến error để chúng ta kiểm tra.

str := string(bs)
fmt.Println(str)

Nếu quá trình đọc file không có lỗi, chúng ta có thể in nội dung của slice ra màn hình.

Hello World

Đoạn code trên có thể viết ngắn lại bằng cách dùng một package khác là io/ioutil như sau:

package main

import (
    "fmt"
    "io/ioutil"
)

func main() {
    bs, err := ioutil.ReadFile("test.txt")
    if err != nil {
        fmt.Println("Error: ", err)
        return
    }
    str := string(bs)
    fmt.Println(str)
}

Hàm ReadFile() trong gói io/ioutil sẽ thực hiện cả việc mở file, đọc nội dung file rồi ghi vào một slice luôn và trả về cho chúng ta.

Tạo file

Để tạo file thì chúng ta dùng hàm Create() trong gói os như sau:

package main

import "os"

func main() {
    file, err := os.Create("test.txt")
    if err != nil {
        fmt.Println("Error: ", err)
        return
    }
    defer file.Close()
    file.WriteString("test")
}

Hàm Create() nhận tên file và tạo ra file đó, sau đó chúng ta có thể dùng hàm WriteString() để ghi những chuỗi text vào file đó.

Xem nội dung thư mục

Chúng ta có thể mở một thư mục bằng hàm Open() trong gói os bằng cách đưa vào đường dẫn thư mục thay vì đường dẫn file, sau đó dùng hàm Readdir() để đọc nội dung thư mục. Ví dụ:

package main 

import (
    "fmt"
    "os"
)

func main() {
    dir, err := os.Open(".")
    if err != nil {
        fmt.Println("Error: ", err)
        return
    }
    defer dir.Close()
    fileInfos, err := dir.Readdir(-1)
    if err != nil {
        fmt.Println("Error: ", err)
        return
    }
    for _, fi := range fileInfos {
        fmt.Println(fi.Name())
    }
}

Chúng ta dùng dấu chấm “.” để viết ngắn cho đường dẫn thư mục hiện tại.

fileInfos, err := dir.Readdir(-1)

Hàm Readdir() nhận vào số lượng thư mục và file mà chúng ta muốn đọc, nếu đưa -1 vào thì đọc tất cả, hàm này sẽ trả về một slice chứa các biến kiểu FileInfo. 

fmt.Println(fi.Name())

Chúng ta có thể lấy tên file hoặc thư mục thông qua hàm Name() trong biến FileInfo.

Go – String

Go có gói strings hỗ trợ các thao tác với chuỗi rất mạnh.

Các hàm thao tác với chuỗi

Gói strings cung cấp một số hàm thao tác với string thường dùng như sau:

package main

import (
    "fmt"
    s "strings"
)

func main() {
    fmt.Println("Contains:  ", s.Contains("test", "es"))
    fmt.Println("Count:     ", s.Count("test", "t"))
    fmt.Println("HasPrefix: ", s.HasPrefix("test", "te"))
    fmt.Println("HasSuffix: ", s.HasSuffix("test", "st"))
    fmt.Println("Index:     ", s.Index("test", "e"))
    fmt.Println("Join:      ", s.Join([]string{"a", "b"}, "-"))
    fmt.Println("Repeat:    ", s.Repeat("a", 5))
    fmt.Println("Replace:   ", s.Replace("foo", "o", "0", -1))
    fmt.Println("Replace:   ", s.Replace("foo", "o", "0", 1))
    fmt.Println("Split:     ", s.Split("a-b-c-d-e", "-"))
    fmt.Println("ToLower:   ", s.ToLower("TEST"))
    fmt.Println("ToUpper:   ", s.ToUpper("test"))  

    fmt.Println("Len: ", len("hello"))
    fmt.Println("Char: ", "hello"[1])
}

Trong đó:

  • Hàm Contains(a, b) cho biết chuỗi b có nằm trong chuỗi a hay không.
  • Hàm Count(a, b) đếm số lần xuất hiện của chuỗi b trong chuỗi a
  • Hàm HasPrefix(a, b) cho biết chuỗi b có bắt đầu từ vị trí đầu tiên trong chuỗi a hay không
  • Hàm HasSuffix(a, b) cho biết chuỗi b có bắt đầu từ vị trí cuối cùng trong chuỗi a hay không
  • Hàm Index(a, b) cho biết vị trí đầu tiên mà chuỗi b xuất hiện trong chuỗi a từ trái qua phải
  • Hàm Join([]a, b) nối các phần tử trong slice a lại thành một chuỗi mới, các phần tử ngăn cách nhau bởi chuỗi b
  • Hàm Repeat(a, b) tạo ra một chuỗi bằng cách lặp lại b lần chuỗi a.
  • Hàm Replace(a, b, c, d) thay các kí tự b trong chuỗi a thành kí tự c với số lần là d, nếu d < 0 thì thay tất cả.
  • Hàm Split(a, b) tách chuỗi a thành các chuỗi nhỏ hơn dựa theo kí tự phân tách là b
  • Hàm ToLower(a) chuyển chuỗi a thành viết hoa
  • Hàm ToUpper(a) chuyển chuỗi a thành viết thường
  • Ngoài ra chúng ta còn có 2 hàm không nằm trong gói strings nhưng cũng làm việc với chuỗi là hàm len() và phép toán [], hàm len() lấy độ dài của chuỗi, phép toán [i] lấy kí tự tại vị trí i.
Contains:  true
Count:     2
HasPrefix: true
HasSuffix: true
Index:     1
Join:      a-b
Repeat:    aaaaa
Replace:   f00
Replace:   f0o
Split:     [a b c d e]
ToLower:   test
ToUpper:   TEST
Len: 5
Char: 101

Go còn cho phép chúng ta chuyển một string thành một slice như sau:

arr := []byte("test")
str := string([]byte{"t", "e", "s", "t"})

Định dạng chuỗi

Chúng ta có thể định dạng dữ liệu xuất ra màn hình. Ví dụ:

package main

import "fmt"
import "os"

type point struct {
    x, y int
}

func main() {
    p := point{1, 2}

    fmt.Printf("%v\n", p)
    fmt.Printf("%v+\n", p)
    fmt.Printf("%#v\n", p)
    fmt.Printf("%T\n", p)

    fmt.Printf("%t\n", true)

    fmt.Printf("%d\n", 123)
    fmt.Printf("%b\n", 14)
    fmt.Printf("%c\n", 33)
    fmt.Printf("%x\n", 456)

    fmt.Printf("%f\n", 78.9)
    fmt.Printf("%e\n", 123400000.0)
    fmt.Printf("%E\n", 123400000.0)

    fmt.Printf("%s\n", "\"string\"")
    fmt.Printf("%q\n", "\"string\"")

    fmt.Printf("%x\n", "hex this")
    fmt.Printf("%p\n", &p)
    fmt.Printf("|%6d|%6d|", 12, 345)
    fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
    fmt.Printf("|%-6.2f|%-6.2f|", 1.2, 3.45)
    fmt.Printf("|%6s|%6s|\n", "foo", "b")
    fmt.Printf("|%-6s|%-6s|", "foo", "b")
 
    s := fmt.Sprintf("a %s", "string")
    fmt.Println(s)
}

Hàm Printf(<định dạng>, a) cho phép chúng ta in dữ liệu theo các dạng nhất định, tham số đầu tiên của hàm này là dạng chuỗi sẽ được in ra, các chuỗi này có kí tự đầu tiên là %, các kí tự tiếp theo mô tả kiểu in của dữ liệu, tham số thứ 2 là dữ liệu, trong ví dụ trên thì chúng ta có định nghĩa một struct tên là point có 2 trường kiểu intx, y, tiếp theo chúng ta in một số dữ liệu ra màn hình bằng hàm Printf():

  • %v: in các giá trị của một biến struct
  • %+v: in các giá trị kèm với tên trường của biến struct
  • %#v: giống %+v kèm theo tên kiểu dữ liệu của struct và tên hàm đã gọi nó
  • %T: in tên struct và tên hàm đã gọi nó
  • %t: in giá trị boolean
  • %d: in số nguyên (hệ 10)
  • %b: in số nguyên dưới dạng số nhị phân (hệ 2)
  • %c: in kí tự dựa theo mã ASCII
  • %x: in số nguyên dưới dạng số thập lục phân (hệ 16) hoặc chuyển một chuỗi thành số thập lục phân
  • %f: in số thập phân
  • %e%E: in số thập phân dưới dạng số mũ
  • %s: in một chuỗi
  • %q: in một chuỗi có 2 cặp dấu nháy kép “”
  • %6d: in một số nguyên, nếu số đó không đủ 6 kí tự thì tự động thêm các khoảng trống vào bên trái cho đủ 6 kí tự
  • %6.2f: in số thập phân, làm tròn đến 2 chữ số thập phân, sau đó nếu phần thập phân và phần nguyên cùng với dấu chấm không đủ 6 kí tự thì tự động thêm các khoảng trống vào bên trái cho đủ 6 kí tự
  • %-6.2f: tương tự với %6.2f nhưng các khoảng trống được thêm vào bên phải
  • %6s: in một chuỗi, nếu chuỗi không đủ 6 kí tự thì thêm các khoảng trống vào bên trái cho đủ
  • %-6s: tương tự %6s nhưng thêm các khoảng trống vào bên phải

Ngoài ra chúng ta có hàm Sprintf() chỉ trả về một chuỗi chứ không in ra màn hình.

{1 2}
{x:1 y:2}
main.point{x:1, y:2}
main.point
true
123
1110
!
1c8
78.900000
1.234000e+08
1.234000E+08
"string"
"\"string\""
6865782074686973
0xc04203c1d0
| 12| 345|| 1.20| 3.45|
|1.20 |3.45 || foo| b|
|foo |b |a string
an error

Go – Package

Một trong những tính năng quan trọng của một ngôn ngữ lập trình là tính năng cho phép tái sử dụng code, trong các phần trước chúng ta đã biết một tính năng cho phép tái sử dụng code đó là hàm (func hay function). Package (hay gói) là một tính năng cho phép tái sử dụng code ở phạm vi rộng hơn, hầu hết các ngôn ngữ cấp cao như Java, C#… đều hỗ trợ package, và cả Go cũng vậy.

Trong các phần trước hầu như đoạn code nào chúng ta cũng có dòng này:

import "fmt"

Dòng đó có nghĩa là chúng ta yêu cầu được sử dụng package fmt, package fmt chứa các hàm làm công việc liên quan đến định dạng dữ liệu ra.  Việc sử dụng package có các lợi ích sau:

  • Giảm thiểu rủi rõ trùng lắp tên hàm. Chẳng hạn như trong gói fmt có hàm Println(), chúng ta có thể định nghĩa một gói khác cũng có hàm Println() nhưng 2 hàm này khác nhau vì chúng nằm ở 2 gói khác nhau.
  • Dễ dàng tổ chức code hơn, do đó giúp chúng ta tìm các hàm cần dùng dễ dàng hơn.
  • Tốc độ biên dịch nhanh, bởi vì trình biên dịch không biên dịch lại code trong các package.

Tạo package

Chúng ta sẽ viết package math chứa hàm Average() tính giá trị trung bình của một dãy số.

Các package được viết ra sẽ được đặt trong thư mục được định nghĩa trong biến môi trường GOPATH, biến này trỏ tới thư mục mà trình biên dịch Go sẽ tìm code trong đó, nếu bạn chưa tạo thì tạo một cái (tìm cách tạo trên Google), chẳng hạn như trong máy mình biến này trỏ tới C:/Project/Go, trong thư mục này sẽ chứa 3 thư mục nữa là src, binpkg, thư mục src là thư mục chứa code, thưc mục bin là thư mục chứa các file .exe, thư mục pkg chứa các file thư viện liên kết tĩnh (đọc thêm Phân biệt thư viện liên kết động và thư viện liên kết tĩnh). Khi viết code cho package thì chúng ta sẽ đặt code đó trong thư mục GOPATH/src.

Bây giờ chúng ta tạo một thư mục là myMath trong thư mục GOPATH/src, trong thư mục này tạo tiếp một thư mục có tên math, trong thư mục myMath/math chúng ta tạo file có tên math.go như sau:

package math

func Average(xs []float64) float64 {
    total := float64(0)
    for _, x :=  range xs {
        total += x
    }
    return total / float64(len(xs))
}

Vây là xong, chúng ta đã tạo gói myMath/math có hàm Average() tính giá trị trung bình của một dãy số.

Bây giờ chúng ta có thể import gói đó để sử dụng, ví dụ:

package main

import "fmt"
import "myMath/math"

func main() {
    xs := []float64{1, 2, 3, 4}
    avg := math.Average(xs)
    fmt.Println(avg)
}

Chạy đoạn code trên chúng ta sẽ ra được kết quả là 2.5. Bạn có thể chạy lệnh go install trong thư mục GOPATH/src/myMath/math để tạo file thư viện nếu muốn.

2.5

Có một số lưu ý như sau:

  • Go cũng có một package có tên là math, tuy chúng ta cũng đặt tên package của mình là math nhưng 2 package này khác nhau vì đường dẫn package của chúng ta là myMath/math.
  • Khi dùng lệnh import thì chúng ta phải ghi rõ ràng đường dẫn ra, như myMath/math, nhưng trong file math.go thì dòng khai báo package chỉ dùng tên ngắn thôi, ví dụ package math.
  • Khi gọi hàm thì chúng ta cũng chỉ dùng tên ngắn, ví dụ math.Average(...). Nếu giả sử bạn dùng cả 2 gói math của Go và myMath/math thì bạn có thể đặt tên giả cho package để phân biệt chúng, ví dụ:
import m "myMath/math"
import "math"

Trong đó m là tên giả của package myMath/math.

  • Các hàm được định nghĩa trong một package nếu có tên có chữ cái đầu tiên viết hoa thì mới có thể gọi được từ package khác, nếu chữ cái đầu tiên viết thường thì không gọi được.
  • Tên package phải trùng với tên thư mục của file chứa nó, ví dụ file math.go phải được đặt trong thư mục math.

Go – Concurrency

Trong phần này chúng ta sẽ tìm hiểu về tính năng xử lý các công việc song song – Concurrency.

Những chương trình lớn đều được xây dựng nên từ những chương trình nhỏ hơn. Chẳng hạn như một webserver sẽ phải tiếp nhận và xử lý các yêu cầu từ browser rồi gửi trả nội dung đó về webserver, thì trong đó mỗi yêu cầu từ browser có thể coi như là một chương trình nhỏ.

Những công việc nhỏ như thế nên được thực hiện song song với nhau, khái niệm này được gọi là Concurrency, Go đưa ra 2 tính năng hỗ trợ concurrency rất mạnh đó là GoroutineChannel.

Goroutine

Goroutine là một hàm có thể chạy đồng thời với các hàm khác. Để một hàm chạy theo kiểu goroutine thì chúng ta thêm từ khóa go vào trước lời gọi hàm, ví dụ:

package main

import "fmt"

func f(n int) {
    for i := 0 ; i < 10 ; i++ {
        fmt.Println(n, ":", i)
    }
}

func main() {
    go f(0)
    var input string 
    fmt.Scanln(&input)
}

Trong đoạn code trên có 2 hàm goroutine, hàm đầu tiên là hàm main(), hàm thứ hai là hàm f() khi được gọi trong câu lệnh go f(0). Nếu như chúng ta gọi hàm f() một cách bình thường thì khi gọi, hàm main() sẽ phải dừng tất cả mọi thứ lại, đợi cho hàm f() thực hiện công việc của nó xong rồi trả lại quyền điều khiển cho hàm main() thì hàm main() mới tiếp tục công việc của nó.

Trong đoạn code trên chúng ta không gọi hàm f() như bình thường mà chúng ta chuyển lời gọi đó thành một goroutine, như thế sau khi gọi, hàm main() vẫn tiếp tục công việc của nó, hàm f() cũng thực hiện công việc của nó một cách song song với hàm main().

Các goroutine rất nhẹ, chúng ta có thể tạo ra cả ngàn goroutine cũng được. Ví dụ:

package main

import "fmt"

func f(n int) {
    for i := 0 ; i < 10 ; i++ {
        fmt.Println(n, ":", i)
    }
}

func main() {
    for i := 0 ; i < 10 ; i++ {
        go f(i)
    }
    var input string
    fmt.Scanln(&input)
}

Cả 2 đoạn code trên sẽ cho kết quả tương tự như sau:

1 : 0
6 : 0
9 : 0
9 : 1
9 : 2
9 : 3
9 : 4
9 : 5
...

Nếu của bạn không giống như vậy mà nó hiển thị các số có thứ tự thì chẳng qua là do CPU chạy nhanh quá, thành ra các goroutine được gọi trước đã chạy xong rồi nên chúng không chạy đồng thời. Bạn thử chạy đoạn code trên nhiều lần sẽ thấy mỗi lần chạy kết quả sẽ khác nhau.

Channel

Channel là tính năng cho phép 2 goroutine giao tiếp/trao đổi dữ liệu với nhau. Ví dụ:

package main

import (
    "fmt"
    "tme"
)

func pinger(c chan string) {
    for i := 0 ; ; i++ {
        c <- "ping"
    }
}

func printer(c chan string) {
    for {
        msg := <- c
        fmt.Println(msg)
        time.Sleep(time.Second * 1)
    }
}

func main() {
    var c chan string = make(chan string)
    go pinger(c)
    go printer(c)
    var input string
    fmt.Scanln(&input)
}

Đoạn code trên sẽ in dòng chữ “ping” vô số lần cho đến khi có người bấm nút Enter. Trong đó chúng ta dùng 2 hàm goroutine là pinger()printer() và 1 channel là c. Về cơ bản thì goroutine là các luồng chương trình chạy xuyên suốt, channel có thể coi như là các “ống” truyền dữ liệu qua lại giữa các luồng chương trình đó.

Untitled

Một channel trong Go chỉ là một biến bình thường, chỉ khác là các goroutine có thể đọc được dữ liệu cũng như ghi dữ liệu vào đó, để khai báo một channel thì chúng ta thêm từ khóa chan vào giữa tên biến và tên kiểu dữ liệu, để khởi tạo một biến channel thì chúng ta dùng hàm make(chan <kiểu dữ liệu>).

Chúng ta dùng dấu <- để đưa dữ liệu vào channel, dấu -> để lấy dữ liệu từ channel. Chẳng hạn c <- "ping" nghĩa là đưa chuỗi “ping” vào channel c, msg := <- c nghĩa là lấy dữ liệu trong channel c gán vào biến msg. 

Việc sử dụng channel cho phép đồng bộ hóa dữ liệu giữa các goroutine bởi vì khi một goroutine truyền dữ liệu vào channel goroutine đó sẽ dừng chương trình của nó và đợi đến khi có một goroutine khác lấy dữ liệu ra khỏi channel rồi thì nó mới tiếp tục. Ví dụ:

package main

import (
    "fmt"
    "time"
)

func pinger(c chan string) {
    for i := 0 ; ; i++ {
        c <- "ping"
    }
}

func ponger(c chan string) {
    for i := 0 ; ; i++ {
        c <- "pong"
    }
}

func printer(c chan string) {
    for {
        msg := <- c
        fmt.Println(msg)
        time.Sleep(time.Second * 1)
    }
}

func main() {
    var c chan string = make(chan string)

    go pinger(c)
    go ponger(c)
    go printer(c)

    var input string
    fmt.Scanln(&input)
}

Đoạn code trên sẽ in các chuỗi “ping” và “pong” liên tiếp nhau.

ping
pong
ping
pong
ping
pong
...

Điều hướng channel

Chúng ta có thể quy định channel chỉ được phép đọc hoặc chỉ được phép ghi dữ liệu vào đó. Ví dụ:

func pinger(c chan<- string)

Dòng code trên quy định channel c chỉ được truyền dữ liệu vào.

func printer(c <-chan string)

Dòng code trên quy định channel c chỉ được phép đọc dữ liệu.

Nếu không quy định hướng đi của channel thì mặc định channel sẽ được phép vừa đọc vừa ghi.

Lệnh Select

Lệnh select trong Go có chức năng giống hệt như lệnh switch, chỉ khác là select thì được dùng với biến channel. Ví dụ:

package main

import "fmt"

func main() {
    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        for {
            c1 <- "from 1"
            time.Sleep(time.Second * 2)
        }
    }()
    go func() {
        for {
            c2 <- "from 2"
            time.Sleep(time.Second * 3)
        }
    }()
    go func() {
        for {
            select {
            case msg1 := c <- c1:
                fmt.println(msg1)
            case msg2 := <- c2:
                fmt.Println(msg2)
            }
        }
    }()
    
    var input string
    fmt.Scanln(&input)
}

Đoạn code trên cứ sau 2 giây sẽ in chuỗi “from 1” và cứ sau 3 giây thì in chuỗi “from 2”. Lệnh select sẽ chọn những channel có dữ liệu để xử lý, nếu không có channel nào có dữ liệu thì chương trình sẽ tạm dừng để “đợi” cho đến khi có một channel có dữ liệu.

Cũng giống như lệnh switch, lệnh channel cũng có trường hợp default, ví dụ:

select {
    case msg1 := <- c1:
        fmt.Println("Message 1", msg1)
    case msg2 := <- c2:
        fmt.Println("Message 2", msg2)
    default:
        fmt.Println("nothing ready")
}

Trong trường hợp này nếu không có channel nào có dữ liệu thì câu lệnh sau default sẽ được chạy.

Buffered Channel

Như đã nói ở trên, các goroutine khi truyền dữ liệu vào channel thì phải có một goroutine khác lấy dữ liệu ra hoặc ngược lại, nếu không các goroutine sẽ đi vào trạng thái “chờ”.

Tuy nhiên chúng ta có thể cho phép goroutine không chờ nữa bằng cách dùng các buffered channel. Buffered Channel tức là channel đó giới hạn dữ liệu vào, ví dụ:

package main

import "fmt"

func main() {
    msg := make(chan string, 2)

    msg <- "buffered"
    msg <- "channel"

    fmt.Println(msg)
    fmt.Println(msg)
}

Trong đoạn code trên chúng ta tạo ra một channel có tên msg, channel này chỉ được phép nhận vào 2 chuỗi. Và vì đây là một buffered channel nên chúng ta có thể truyền dữ liệu vào mà không cần đợi một goroutine khác lấy dữ liệu ra.

buffered
channel

Go – Struct và Interface

Trong phần này chúng ta sẽ tìm hiểu về khái niệm struct và interface trong Go. Đây là các khái niệm trong lập trình hướng đối tượng (OOP), nếu bạn chưa từng làm việc với OOP thì bạn nên tham khảo thêm vì lý thuyết OOP trên mạng.

Giả sử chúng ta có đoạn code tính diện tích hình tròn và diện tích hình chữ nhật từ các điểm trên mặt phẳng như sau:

package main

import (
    "fmt"
    "math"
)

func distance(x1, y1, x2, y2 float64) float64 {
    a := x2 - x1
    b := y2 - y1
    return math.Sqrt(a*a + b*b)
}

func rectangleArea(x1, y1, x2, y2 float64) float 64 {
    l := distance(x1, y1, x2, y2)
    w := distance(x1, y1, x2, y1)
    return l * w
}

func circleArea(x, y, r float64) float64 {
    return math.Pi * r*r;
}

func main() {
    var rx1, ry1 float64 = 0, 0
    var rx2, ry2 float64 = 10, 10
    var cx, cy, cr float64 = 0, 0, 5

    fmt.Println(rectangleArea(rx1, ry1, rx2, ry2))
    fmt.Println(circleArea(cx, cy, cr))
}

Thoạt nhìn thì bài toán này có vẻ đơn giản, tuy nhiên khi bạn muốn tính diện tích của khoảng vài chục hình vuông/hình tròn, lúc này bản thân việc đặt tên biến đã muốn mệt chứ chưa nói đến việc tính toán, dĩ nhiên bạn có thể dùng array, slice… nhưng giả sử bạn muốn hình dung trong đầu hình nào ở vị trí số mấy trong mảng là cũng khá khó khăn rồi. Do đó bạn cần dùng kiểu struct để có thể quản lý tốt hơn.

Struct

Một struct là một kiểu dữ liệu đặc biệt, kiểu này chứa biến thuộc các kiểu dữ liệu khác, các biến ở đây thường được gọi là các trường hoặc các thuộc tính… Ví dụ chúng ta định nghĩa struct có tên Circle (hình tròn) và Rectangle (hình chữ nhật) như sau:

type Circle struct {
    x float64
    y float64
    z float64
}

type Rectangle struct {
    x1 float64
    y1 float64
    x2 float64
    y2 float64
}

Để định nghĩa một struct thì chúng ta dùng từ khóa type, từ khóa này báo cho Go biết là chúng ta đang định nghĩa một kiểu dữ liệu mới, theo sau là tên kiểu dữ liệu do chúng ta tự đặt, tiếp theo là từ khóa struct để báo cho Go biết là chúng ta đang định nghĩa một struct, cuối cùng là danh sách các trường của struct này. Mỗi trường chúng ta khai báo gồm tên trường và tên kiểu dữ liệu. Ngoài ra chúng ta có thể khai báo ngắn gọn lại như sau:

type Circle struct {
    x, y, r float64 
}

type Rectangle struct {
    x1, y1, x2, y2 float64
}

Khai báo biến kiểu struct

Chúng ta khai báo biến kiểu struct giống như khai báo một biến bình thường:

var c Circle
var r Rectangle

Lúc này biến c có kiểu dữ liệu là Circle, các trường của biến c sẽ có kiểu dữ liệu mặc định là 0 với int, 0.0 với float, “” với string, nil với con trỏ, biến r cũng tương tự như vậy.  Hoặc khai báo bằng cách dùng hàm new():

c := new(Circle)
r := new(Rectangle)

Nếu muốn khởi tạo và gán giá trị cho các trường luôn thì chúng ta làm như sau:

c := Circle{x: 0, y : 0, r : 5}
r := Rectangle{x1 : 0, y1 : 10, x2 : 0, y2 : 10}

Hoặc chúng ta không cần ghi tên trường ra nhưng phải truyền kiểu dữ liệu theo đúng thứ tự đã định nghĩa:

c := Circle{0, 0, 5}
r := Rectangle{0, 0, 10, 10}

Trường

Để thao tác các trường thì chúng ta dùng dấu chấm “.” như sau:

fmt.Println(c.x, c.y, c.z)
c.x = 10
c.y = 5

Định nghĩa struct trong một hàm cũng giống như các kiểu dữ liệu khác:

func circleArea(c Circle) float64 {
    return math.Pi * c.r*c.r
}

func rectangleArea(r Rectangle) float64 {
    l := distance(r.x1, r.y1, r.x1, r.y2)
    w := distance(r.x1, r.y1, r.x2, r.y1)
    return l * w
}

Khi gọi hàm chúng ta cũng chỉ truyền tên biến struct vào là được:

c := Circle{0, 0, 5}
r := Rectangle{0, 0, 10, 10}
fmt.Println(circleArea(c))
fmt.println(rectangleArea(r))

Cũng giống như các kiểu dữ liệu khác, khi truyền một struct vào hàm, thực chất Go sẽ sao chép biến đó vào trong tham số của hàm chứ không trực tiếp thao tác với hàm, do đó nếu muốn hàm thực hiện các thao tác trên chính struct được truyền vào thì chúng ta phải truyền con trỏ (hoặc địa chỉ bộ nhớ của biến) vào hàm bằng cách dùng phép toán &:

func circleArea(c *Circle) float64 {
    return math.Pi * c.r*c.r
}

func main() {
    c := Circle{0, 0, 5}
    fmt.Println(circleArea(&c))
}

Phương thức

Phương thức là các hàm của riêng một struct, khi dùng thì chỉ có các biến có kiểu struct đó mới gọi được hàm. Ví dụ:

func (c *Circle) area() float64 {
    return math.Pi * c.r*c.r
}

func (r *Rectangle) area() float64 {
    l := distance(r.x1, r.y1, r.x1, r.y2)
    w := distance(r.x1, r.y1, r.x2, r.y1)
    return l * w
}

Để định nghĩa một phương thức thì ở giữa từ khóa func và tên hàm chúng ta khai báo struct sở hữu phương thức đó theo cú pháp giống như khai báo tham số của hàm. Để gọi một phương thức của một struct thì chúng ta cũng dùng dấu chấm “.” như sau:

fmt.Println(c.area())
fmt.Println(r.area())

Một phương thức của một struct có thể đọc giá trị của các trường trong struct đó, do đó dùng struct sẽ làm cho việc code trở nên dễ dàng hơn rất nhiều, chúng ta không còn phải truyền các biến không cần thiết vào hàm nữa, và hay hơn là không phải truyền con trỏ. Ngoài ra phương thức của một struct chỉ có struct đó mới dùng được nên việc đặt tên cũng dễ dàng hơn nhiều, thay vì đặt tên circleArea()rectangleArea(), chúng ta chỉ cần đặt là area() là đủ.

Interface

Trong các ví dụ trên, chúng ta đã định nghĩa 2 struct là Rectangle (hình chữ nhật) và Circle (hình tròn), cả 2 struct này đều một phương thức tính diện tích có tên giống nhau là area(). Chúng ta có thể “gộp chung” 2 phương thức đó vào một kiểu dữ liệu khác có tên là Interface:

type Shape interface {
    area() float64
}

Trong ví dụ trên chúng ta định nghĩa một Interface có tên Shape, interface này có một phương thức là area(). Để định nghĩa một interface thì cũng giống như định nghĩa một struct, chúng ta dùng từ khóa type, tiếp đến là tên interface rồi đến từ khóa interface, sau đó là danh sách các phương thức trong cặp dấu ngoặc nhọn {}.

Interface thực ra cũng không hẳn là một kiểu dữ liệu như struct vì interface chỉ chứa các phương thức chứ không chứa các trường, interface cũng không có phần định nghĩa phương thức ở ngoài như các struct, chúng chỉ chứa tên phương thức là hết. Vậy thì việc sử dụng interface có gì hay? Câu trả lời là chúng ta có thể dùng interface để thực hiện tính toán trên nhiều kiểu struct khác nhau mà không quan tâm các struct đó là gì. Ví dụ:

package main 

import (
    "fmt"
    "math"
)

type Shape interface {
    area() float64
}

type Circle struct {
    x, y, r float64
}

type Rectangle struct {
    x1, y1, x2, y2 float64
}

func distance(x1, y1, x2, y2 float64) float64 {
    a := x2 - x1
    b := y2 - y1
    return math.Sqrt(a*a + b*b)
}

func (c *Circle) area() float64 {
    return math.Pi * c.r*c.r
}

func (r *Rectangle) area() float64 {
    l := distance(r.x1, r.y1, r.x1, r.y2)
    w := distance(r.x1, r.y1, r.x2, r.y1)
    return l * w
}

func totalArea(shapes ...Shape) float64 {
    var area float64
    for _, s := range shapes {
    area += s.area()
    }
    return area
}

func main() {
    c := Circle{0, 0, 5}
    r := Rectangle{0, 0, 10, 10}
 
    fmt.Println(totalArea(&c, &r))
}

Trong ví dụ trên, chúng ta định nghĩa hàm totalArea() có chức năng tính tổng diện tích của bất cứ hình nào, hàm này nhận vào tham số là kiểu Shape, nhưng chúng ta có thể truyền vào kiểu Circle hoặc kiểu Rectangle đều được, nếu chúng ta truyền vào kiểu Circle, khi gọi phương thức area() thì Go sẽ gọi phương thức area() của struct Circle, và ngược lại khi truyền vào Rectangle thì gọi phương thức area() của Rectangle.

178.53981633974485