Hỏi - đáp Nơi cung cấp thông tin nghề nghiệp và giải đáp những thắc mắc thường gặp của bạn

Một số thủ thuật và mẹo hay khi lập trình Golang

Những lời khuyên mở đầu

  • Tạm gạt bỏ hết những kiến thức về hướng đối tượng sang một bên, làm quen cách sử dụng interface.
  • Học các sử dụng Go để lập trình và làm mọi thứ chứ đừng áp đặt những logic hay cú pháp của ngôn ngữ khác vào Go.
  • Nắm bắt được các tính chất cảu ngôn ngữ: simplicity, concurrency, và composition.
  • Đọc tất cả các tài liệu hữu ích trên golang.org.
  • Luôn luôn sử dụng gofmt.
  • Tham khảo, tìm tòi nhiều mã nguồn được code bằng Go.
  • Tìm hiểu và làm quen với các công cụ và tiện ích.

Một số cách để import một package

Dưới đây là một số cách giúp các bạn import một package Go vào project. Mình ví dụ với package fmt:

  • import format "fmt" - Tạo một định danh đại diện cho fmt. Ưu tiên tất cả các nội dung trong package fmt với định dạng.
  • import . "fmt" - Cho phép truy cập trực tiếp nội dung của package không cần phải gọi fmt
  • import _ "fmt" - Loại bỏ các cảnh báo của trình biên dịch liên quan đến fmt nếu nó không được sử dụng và thực thi các hàm khởi tạo nếu có. Phần còn lại của fmt không thể truy cập.

goimports

Goimports là một công cụ dùng để cập nhật những dòng import trong Go, thêm những dòng import còn thiếu và loại bỏ những dòng không sử dụng.

Nó tương tự như gofmt nhưng kèm theo cả formet code và sửa lỗi import.

Tổ chức mã nguồn

Go là một ngôn ngữ lập trình khá dễ học nhưng lại là thách thức lớn cho các lập trình viên trong việc tổ chức mã nguồn. Rails trở nên phổ biến bởi nhiều lý do và scaffolding là một trong số đó. Nó chỉ dẫn cho những lập trình viên mới hướng đi rõ ràng và nơi họ có thể đặt mã nguồn và logic để làm theo.

Ở một mức độ nào đó, Go thực hiện điều tương tự bằng cách cung cấp cho các nhà phát triển công cụ tuyệt vời như go fmt và có một trình biên dịch chặt chẽ để kiểm tra và không biên dịch những biến hoặc các câu lệnh import không sử dụng.

Constructors tùy chỉnh

Một câu hỏi mà mình thường nghe nhiều nhất đó là khi nào thì chúng ta nên sử dụng constructortùy chỉnh như là NewJob. Câu trả lời của mình lf trong hầu hết các trường hợp thì bạn đều không cần đến nó. Tuy nhiên, bất cứ khi nào mà bạn cần thiết lập một giá trị khởi tạo ban đầu và bạn có một tập các giá trị mặc định thì mới nên dùng constructor. Ở ví dụ bên dưới, sử dụng constructor sẽ rất hợp lý, chúng ta có thể thiết lập logger mặc định như sau:

package main
import (
"log"
"os"
)
type Job struct {
Command string
*log.Logger
}
func NewJob(command string) *Job {
return &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
}
func main() {
NewJob("demo").Print("starting now...")
}

Sets

Một số trường hợp bạn sẽ muốn tạo một tập hợp mới từ một mảng bao gồm các giá trị unique (không trùng lặp). Trong các ngôn ngữ khác, bạn thường phải thiết lập một data structure không cho phép các giá trị trùng lăp. Go thì không có tính năng đó, tuy nhiên nó không khó để viết một hàm có chức năng như vậy, các bạn tham khảo mã nguồn dưới đây:

// UniqStr returns a copy if the passed slice with only unique string results.
func UniqStr(col []string) []string {
m := map[string]struct{}{}
for _, v := range col {
if _, ok := m[v]; !ok {
m[v] = struct{}{}
}
}
list := make([]string, len(m))
i := 0
for v := range m {
list[i] = v
i++
}
return list
}

Chạy thử code trên ở đây.

Mình đã sử dụng một số thủ thuật thú vị, đầu tiên là map các structs rỗng.

m := map[string]struct{}{}

Chúng ta tạo một map với các keys là những giá trị mà chúng ta muốn nó là duy nhất,

m := map[string]bool{}

Tuy nhiên, mình sử dụng structure rỗng bởi vì nó sẽ nhanh như boolean và không chiếm nhiều bộ nhớ.

Thủ thuật thứ 2:

if _, ok := m[v]; !ok {
m[v] = struct{}{}
}

Những gì mình đang làm ở đây đơn giản là kiểm ra nếu có một giá trị liên quan tới key v trong map m, chúng ta không cần quan tâm đến giá trị của chính nó, nhưng chúng ta biết rằng chúng ta vẫn chưa có giá trị vì thế cần phải thêm vào.

Một khi chúng ta đã có được một map với các unique keys, chúng ta có thể xuất nó ra thành một slice mới kiểu string và trả về kết quả.

Dưới đây là một số test case cho hàm này, như các bạn có thể thấy mình sử dụng table test

Here is the test for this function, as you can see, I used a table test, là một cách để chạy unit test trong Go:

func TestUniqStr(t *testing.T) {
data := []struct{ in, out []string }{
{[]string{}, []string{}},
{[]string{"", "", ""}, []string{""}},
{[]string{"a", "a"}, []string{"a"}},
{[]string{"a", "b", "a"}, []string{"a", "b"}},
{[]string{"a", "b", "a", "b"}, []string{"a", "b"}},
{[]string{"a", "b", "b", "a", "b"}, []string{"a", "b"}},
{[]string{"a", "a", "b", "b", "a", "b"}, []string{"a", "b"}},
{[]string{"a", "b", "c", "a", "b", "c"}, []string{"a", "b", "c"}},
}
for _, exp := range data {
res := UniqStr(exp.in)
if !reflect.DeepEqual(res, exp.out) {
t.Fatalf("%q didn't match %q\n", res, exp.out)
}
}
}

Test chương trình ở đây

Quản lý các dependency package

Thật không may là Go không đi kèm với hệ thống quản lý các dependency package. những package không được đánh dấu phiên bản và những dependencies với phiên bản không được đánh dấu.

Thách thức ở đây là nếu bạn có nhiều lập trình viên cũng làm việc trên một dự án. Bạn muốn tất họ làm việc trên cùng một phiên bản của các dependencies và bạn muốn chắc chắn rằng mọi thứ đều chạy ổn. Và càng khó hơn nữa nếu bạn có nhiều projects sử dụng các phiên bản khác nhau của cùng một dependency. Đây là trường hợp thường thấy nhất ở môi trường CI (Tích hợp liên tục).

Cộng đồng Go đã đưa ra rất nhiều giải pháp cho vấn đề này. Nhưng với mình, không có biện pháp nào là thật sự tốt vì thế chúng ta sẽ dùng đến giải pháp đơn giản hoạt động nhất mà chúng ta tìm thấy: gpm.

Gpm là một bash scrit đơn giản, chúng ta chỉnh sửa nó một ít sau đó có thể bỏ script đó vào trong mỗi repo. Bash script sử dụng file tùy chỉnh gọi là Godeps.

Khi chúng ta chuyển sang một project khác, chúng ra chạy gpm script để pull về hay thiết lập đúng revision cho mỗi package.

Trong môi trường CI, chúng ta thiết lập GOPATH đến thư mục của project trước khi chạy test suite để những packages không thể chia sẻ giữa những project với nhau.

Sử dụng errors

Errors là một pattern rất quan trọng trong Go và khi mới bắt đầu học Golang, các lập trình viên sẽ rất ngạc nhiên bởi các hàm trả về 1 giá trị và 1 lỗi

Go không có khái niệm exception (biệt lệ) như bạn thường thấy trong các ngôn ngữ lập trình khác. Go có một thứ gọi là panic nhưng tên của nó khi dịch ra nghe có vẻ rất đặc biệt và ko an toàn cho lắm.

Xử lý lỗi trong Golang thoạt nhìn có vẻ cồng kềnh và lặp đi lặp lại, nhưng nhanh chóng trở thành một phần trong cách chúng ta nghĩ. Thay vì bắt các ngoại lệ có thể hoặc không thể xảy ra, lỗi trong Go là một phần của phản hồi và được thiết kế để xử lý bởi người gọi. Bất cứ khi nào một chức năng có thể tạo ra một lỗi, phản hồi của nó sẽ chứa một tham số lỗi.

Điểm sơ qua về tối ưu hóa bằng trình biên dịch

Khi biên dịch các bạn có thể truyền vào các cờ để xem tối ưu hóa đang được áp dụng và một số khía cạnh của quản lý bộ nhớ. Đây là tính năng nâng cao, hầu hết dành cho người hiểu được việc tối ưu hóa bằng trình biên dịch.

Hãy xem qua một số code mẫu dưới đây:

package main
import "fmt"
type User struct {
Id int
Name, Location string
}
func (u *User) Greetings() string {
return fmt.Sprintf("Hi %s from %s",
u.Name, u.Location)
}
func NewUser(id int, name, location string) *User {
id++
return &User{id, name, location}
}
func main() {
u := NewUser(42, "Matt", "LA")
fmt.Println(u.Greetings())
}

Build file của bạn (ở đây là t.go) và truyền vào một số cờ:

$ go build -gcflags=-m t.go
# command-line-arguments
./t.go:15: can inline NewUser
./t.go:21: inlining call to NewUser
./t.go:10: leaking param: u
./t.go:10: leaking param: u
./t.go:12: (*User).Greetings ... argument does not escape
./t.go:15: leaking param: name
./t.go:15: leaking param: location
./t.go:17: &User literal escapes to heap
./t.go:15: leaking param: name
./t.go:15: leaking param: location
./t.go:21: &User literal escapes to heap
./t.go:22: main ... argument does not escape

Trình biên dịch chỉ ra rằng chúng ta có thể inline hàm NewUser được định nghĩa ở dòng 15 và inline nó ở dòng 21.

Về cơ bản, trình biên dịch di chuyển phần thân hàm của NewUser đến nơi mà nó được gọi và vì thế sẽ tránh được chi phí của một lệnh gọi hàm nhưng sẽ tăng kích thước file.

Chúng ta có thể tối ưu như sau:

func main() {
id := 42 + 1
u := &User{id, "Matt", "LA"}
fmt.Println(u.Greetings())
}

Thiết lập build id sử dụng git’s SHA

Việc này sẽ rất hưu ích khi bạn thêm id vào built file của project. Cá nhân mình thì thường sử dụng SHA1 của git commit. Các bạn có thể lấy version từ SHA1 của commit mới nhất sử dụng câu lệnh sau:

git rev-parse --short HEAD

Bước tiếp theo là thiết lập exported variable khi biên dịch sử dụng cờ -ldflags

package main
import "fmt"
// compile passing -ldflags "-X main.Build <build sha1>"
var Build string
func main() {
fmt.Printf("Using build: %s\n", Build)
}

Lưu đoạn code ở trên trong file example.go. Nếu bạn chạy mã nguồn ở trên, Build sẽ không được thiết lập, vì thế nên bạn phải sử dụng go build và cờ -ldflags.

$ go build -ldflags "-X main.Build a1064bc" example.go

Chạy thử để xem kết quả nào:

$ ./example
Using build: a1064bc

Xem tất cả những packages mà dự án của bạn đã import

Một các đơn giản để xem tất cả các packages được import trong dự án là sử dụng công cụ go list. Chạy câu lệnh xau trong project của bạn:

$ go list -f '{{join .Deps "\n"}}' |
xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}'

Chúng ta test thử với repo clirescue:

$ cd $GOPATH/src/github.com/GoBootcamp/clirescue
$ go list -f '{{join .Deps "\n"}}' |
xargs go list -f '{{if not .Standard}}{{.ImportPath}}{{end}}'
github.com/GoBootcamp/clirescue/cmdutil
github.com/GoBootcamp/clirescue/trackerapi
github.com/GoBootcamp/clirescue/user
github.com/codegangsta/cli

Nếu bạn muốn liệt kê danh sách bao gồm cả các package thông dụng thì sử dụng

$ go list -f '{{join .Deps "\n"}}' |  xargs go list -f '{{.ImportPath}}'

Constants

Một số trường hợp constants sử dụng chuỗi

const (
CCVisa = "Visa"
CCMasterCard = "MasterCard"
CCAmericanExpress = "American Express"
)

Những trường hợp khác chúng ta chỉ cần phân biệt giữa các constants với nhau, chúng ta không quan tâm tới chuỗi giá trị của những constants đó bởi team Marketing sẽ thay đổi nó liên tục. Trong trường hợp này chúng ta nên sử dụng giá trị số để làm đại diện cho hằng

const (
CategoryBooks = 0
CategoryHealth = 1
CategoryClothing = 2
)

Chúng ta có thể chọn bất cữ số nào, miễn là chúng khác nhau.

Hằng số rất quan trọng nhưng nó có thể khó để bảo trì. Trong một số ngôn ngữ như Ruby, lập trình viên thường tránh sử dụng chúng. Tuy nhiên trong Go, hằng số có nhiều điểm thú vị, nếu sử dụng đúng cách sẽ làm cho mã nguồn chuyên nghiệp hơn và dễ bảo trì hơn nữa.

Auto Increment Constants

Một cú pháp sử dụng cho trường hợp này là iota, nó giúp đơn giản hoá trong việc tạo danh sách hằng số tự tăng.

const (
CategoryBooks = iota // 0
CategoryHealth // 1
CategoryClothing // 2
)


Via vgolang.com