Cơ bản về Golang language (Phần 2)
Xin chào các bạn đến với phần tiếp theo về việc tìm hiểu cơ bản về việc sử dụng Go, ở phần này mình xin giới thiệu những tính năng còn lại của Go. Ở phần trước, mình đã nói về các tính năng: 1: Slice 2: Map 3: Struct 4: Interface 5: Hàm 6: Con trỏ Khi chúng ta gọi một hàm và truyền tham số vào ...
Xin chào các bạn đến với phần tiếp theo về việc tìm hiểu cơ bản về việc sử dụng Go, ở phần này mình xin giới thiệu những tính năng còn lại của Go. Ở phần trước, mình đã nói về các tính năng: 1: Slice 2: Map 3: Struct 4: Interface 5: Hàm
6: Con trỏ
Khi chúng ta gọi một hàm và truyền tham số vào đó, giá trị của tham số đó sẽ được sao chép vào trong hàm đó, ví dụ:
package main import "fmt" func zero(x int) { x = 0 } func main() { x := 5 zero(x) fmt.Println(x) } Kết quả là 5
Trong đoạn code trên, hàm zero() có tác dụng gán cho biến x giá trị là 0. Trong hàm main() chúng ta khai báo một biến x có giá trị là 5 rồi truyền vào trong hàm zero(), sau đó chúng ta in giá trị của biến x ra màn hình, kết quả vẫn bằng 5. Lý do là vì giá trị của biến x trong hàm main() được sao chép vào tham số x của riêng hàm zero() chứ hàm zero() không nhận một biến x nào cả. Tuy nhiên nếu chúng ta muốn hàm zero() thao tác trực tiếp luôn với biến x của hàm main() thì chúng ta phải dùng đến con trỏ
package main import "fmt" func zero(xPtr *int) { *xPtr = 0 } func main() { x := 5 zero(&x) fmt.Println(x) }
Con trỏ là một loại biến đặc biệt, được dùng để lưu trữ địa chỉ của biến khác trong bộ nhớ RAM chứ không lưu trữ một giá trị cụ thể nào. Để khai báo một biến con trỏ thì chúng ta thêm dấu sao " * " vào trước tên kiểu dữ liệu. Khi chúng ta in giá trị của một biến con trỏ ra màn hình thì giá trị đó sẽ là một số ở hệ hexa (hệ 16), đó là địa chỉ bộ nhớ mà con trỏ này đang "trỏ" tới. Khi gán giá trị cho biến con trỏ, chúng ta cũng phải đưa vào đó một địa chỉ bộ nhớ nào đó chứ không đưa một giá trị vào, để lấy địa chỉ bộ nhớ của một biến thì chúng ta dùng dấu "&" trước tên biến. Ngoài chức năng khai báo biến con trỏ, dấu " * " còn có tác dụng lấy giá trị của địa chỉ bộ nhớ mà con trỏ đang tham chiếu tới, ngược lại chúng ta cũng có thể gán giá trị cho địa chỉ đó thông qua dấu " * ". Ví dụ:
package main import "fmt" func main() { var x *int var y int y = 0 x = &y fmt.Println(x) fmt.Println(&y) fmt.Println(*x) fmt.Println(y) *x = 1 fmt.Println(*x) fmt.Println(y) }
7: Package
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, bin và pkg, 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) }
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"
8: Xử lý lỗi
1: 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 result và err, 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. 2: 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 đó.
Kết luận
Trên đây là những lý thuyết và một số ví dụ nhỏ của mình về Go, nếu có sai sót nào mong các bạn bình luận góp ý để bài của mình có thể hoàn thiện hơn, Cảm ơn các bạn đã theo dõi.