Khai báo hàm
Một khai báo hàm (function) gồm func, một tên, các tham số, kiểu kết quả và phần thân. Kiểu đứng sau tên tham số, không đứng trước.
add nhận hai giá trị int và trả về một int. greet không trả về gì, nên chữ ký của nó không có kiểu kết quả và phần thân không cần return.
Một hàm có khai báo kết quả phải kết thúc bằng một câu lệnh kết thúc. Nếu có nhánh nào đi tới dấu ngoặc nhọn đóng mà không trả về, trình biên dịch sẽ dừng với lỗi missing return.
Tham số dùng chung kiểu
Các tham số liên tiếp cùng kiểu có thể dùng chung một tên kiểu. func add(a, b int) int có nghĩa hoàn toàn giống func add(a int, b int) int. Kết hợp cũng được:
func scale(x, y float64, factor int) (float64, float64)
Ở đây x và y là float64 còn factor là int.
Giá trị trả về
Một hàm có thể trả về không, một hoặc nhiều giá trị. Nhiều kết quả được đặt trong ngoặc tròn, và bên gọi nhận chúng bằng phép gán nhiều biến:
Chương trình này in ra 3 2. Cặp (value, error) là cách dùng phổ biến nhất của tính năng này trong Go, và có trang riêng về nhiều giá trị trả về.
Đối số được sao chép
Go truyền mọi đối số theo giá trị. Hàm làm việc trên một bản sao, nên việc gán cho tham số không bao giờ thay đổi biến của bên gọi.
Đầu ra là:
{1 2}
{100 2}
[99 2 3]
Dòng thứ ba khiến nhiều người bất ngờ. Một giá trị slice là một header nhỏ (con trỏ, độ dài, sức chứa). Bản sao trỏ tới cùng một mảng nền, nên ghi vào s[0] sẽ ghi vào phần tử của bên gọi. Nhưng append bên trong hàm có thể cấp phát một mảng mới, và bên gọi không bao giờ thấy header mới đó. Nếu một hàm làm slice dài thêm, hãy trả về slice mới. Logic tương tự áp dụng cho map: hàm có thể thêm khóa mà bên gọi nhìn thấy, vì giá trị map trỏ tới dữ liệu dùng chung.
Dùng tham số con trỏ khi hàm cần sửa biến của bên gọi, hoặc khi giá trị là một struct lớn mà bạn không muốn sao chép ở mỗi lần gọi.
Hàm là giá trị
Một hàm có kiểu, được viết không kèm tên: func(int, int) int. Bạn có thể lưu một hàm vào biến, đặt nó vào map hoặc slice, và truyền nó cho một hàm khác.
square là một hàm ẩn danh được gán cho một biến. Hàm ẩn danh cũng có thể đọc và thay đổi biến ở phạm vi xung quanh, và điều đó khiến chúng trở thành closure.
Một kiểu hàm có tên giúp chữ ký dễ đọc hơn khi cùng một dạng xuất hiện ở nhiều nơi:
type Transform func(int) int
func apply(xs []int, f Transform) []int
Giá trị zero của một kiểu hàm là nil. Gọi một hàm nil sẽ panic với invalid memory address or nil pointer dereference, nên hãy kiểm tra các callback không bắt buộc trước khi gọi. Giá trị hàm chỉ có thể so sánh với nil, không bao giờ so sánh được với nhau.
Đệ quy
Một hàm có thể tự gọi chính nó. Go không có tối ưu hóa tail call, nhưng stack của goroutine tự mở rộng khi cần, nên độ sâu đệ quy thông thường không thành vấn đề.
for i := range 6 đếm từ 0 đến 5 và cần Go 1.22 trở lên.
Những gì hàm Go không có
- Không có nạp chồng (overloading). Hai hàm trong cùng một package không thể trùng tên, kể cả khi tham số khác nhau.
- Không có đối số mặc định hay đối số có tên. Mọi tham số đều được truyền ở mỗi lần gọi, theo đúng thứ tự. Một struct cấu hình cho bạn các trường có tên và không bắt buộc:
NewServer(Config{Port: 8080}). - Không có hàm có tên lồng nhau. Bên trong thân hàm, bạn chỉ có thể khai báo hàm ẩn danh, thường được gán cho một biến.
Danh sách đối số có độ dài thay đổi được hỗ trợ qua hàm variadic.
Hàm được export và không được export
Một hàm có tên bắt đầu bằng chữ in hoa (Parse) được export và có thể gọi từ package khác. Tên viết thường (parse) chỉ nhìn thấy được bên trong package của nó. Go không có từ khóa public hay private; viết hoa hay viết thường là toàn bộ quy tắc.
Những lỗi thường gặp
- Mong đợi thay đổi tham số sẽ đến được bên gọi. Gán cho tham số chỉ thay đổi bản sao. Hãy trả về giá trị mới hoặc nhận một con trỏ.
- Gọi append bên trong hàm rồi bỏ qua kết quả.
func add(s []int) { s = append(s, 1) }không ảnh hưởng đến độ dài slice của bên gọi. Hãy viếtfunc add(s []int) []intvà dùngs = add(s). - Kết quả không được dùng. Go cho phép bỏ qua giá trị trả về.
go vetkhông đánh dấu một lỗi bị bỏ qua, nhưng các linter nhưerrcheck(thuộcgolangci-lint) thì có. Chỉ dùng_ =khi việc bỏ qua là một quyết định có chủ ý.
Câu hỏi thường gặp
Định nghĩa hàm trong Go như thế nào?
Dùng từ khóa func, một tên, danh sách tham số với kiểu đứng sau tên, và kiểu kết quả: func add(a, b int) int { return a + b }. Hàm không có kết quả thì bỏ phần kiểu, còn hàm có nhiều kết quả thì liệt kê chúng trong ngoặc tròn: func divmod(a, b int) (int, int).
Go truyền đối số theo giá trị hay theo tham chiếu?
Luôn theo giá trị. Hàm nhận một bản sao của mỗi đối số. Với một int hay một struct, bản sao đó độc lập với biến của bên gọi. Slice, map, channel và con trỏ là những giá trị nhỏ trỏ tới dữ liệu dùng chung, nên thay đổi trên các phần tử mà chúng trỏ tới sẽ hiện ra ở bên gọi, còn việc gán lại chính tham số thì không. Hãy truyền con trỏ (*T) khi hàm cần thay đổi biến của bên gọi.
Có thể truyền hàm làm tham số trong Go không?
Có. Hàm là giá trị với một kiểu như func(int) int. Khai báo một tham số thuộc kiểu đó và truyền vào bất kỳ hàm nào có chữ ký khớp, có tên hay ẩn danh đều được: func apply(xs []int, f func(int) int).
Go có hỗ trợ nạp chồng hàm hay tham số mặc định không?
Không. Mỗi tên hàm trong một package phải là duy nhất, và mọi tham số đều phải được truyền ở mỗi lần gọi. Các cách thay thế thường dùng là đặt tên khác nhau (NewServer, NewServerWithTLS), một struct cấu hình, tham số variadic, hoặc functional options.