Loại
Trong phần loại dữ liệu trước đó đã giới thiệu đơn giản tất cả các loại dữ liệu built-in trong Go những loại cơ sở built-in này là nền tảng cho các loại tùy chỉnh tiếp theo. Go là một ngôn ngữ loại tĩnh điển hình tất cả các loại biến đều được xác định trong thời gian biên dịch và sẽ không thay đổi trong suốt vòng đời của chương trình phần này sẽ giới thiệu đơn giản hệ thống loại và cách sử dụng cơ bản của Go.
Tĩnh mạnh loại
Go là một ngôn ngữ tĩnh mạnh loại tĩnh có nghĩa là tất cả các loại biến của Go đã được xác định từ trong thời gian biên dịch và sẽ không thay đổi trong vòng đời của chương trình mặc dù khai báo biến ngắn trong Go có cách viết hơi giống ngôn ngữ động nhưng loại biến của nó được trình biên dịch tự động suy ra sự khác biệt cơ bản nhất là loại của nó một khi đã suy ra sẽ không thay đổi nữa ngôn ngữ động thì hoàn toàn ngược lại. Do đó mã dưới đây hoàn toàn không thể biên dịch thành công vì a là biến loại int không thể gán giá trị chuỗi.
func main() {
var a int = 64
a = "64"
fmt.Println(a) // cannot use "64" (untyped string constant) as int value in assignment
}Mạnh loại có nghĩa là thực hiện kiểm tra loại nghiêm ngặt trong chương trình khi xuất hiện tình huống không khớp loại sẽ lập tức nói cho lập trình viên không nên làm như vậy chứ không giống như ngôn ngữ động cố gắng suy ra kết quả có thể. Do đó mã dưới đây không thể biên dịch thành công vì hai loại khác nhau không thể thực hiện phép toán.
func main() {
fmt.Println(1 + "1") // invalid operation: 1 + "1" (mismatched types untyped int and untyped string)
}Loại hậu trí
Go tại sao lại đặt khai báo loại ở phía sau chứ không phải phía trước phần lớn là rút kinh nghiệm từ C lấy một ví dụ của chính thức để hiển thị hiệu quả đây là một con trỏ hàm
int (*(*fp)(int (*)(int, int), int))(int, int)Thành thật mà nói không nghiêm túc xem rất khó biết đây là loại gì trong Go cách viết tương tự như sau
f func(func(int,int) int, int) func(int, int) intCách khai báo của Go luôn tuân theo nguyên tắc tên ở phía trước loại ở phía sau đọc từ trái sang phải đại khái ánh mắt đầu tiên có thể biết đây là một hàm và giá trị trả về là func(int,int) int. Khi loại trở nên phức tạp hơn loại hậu trí về khả năng đọc sẽ tốt hơn nhiều Go trong nhiều khía cạnh thiết kế đều là để phục vụ cho khả năng đọc.
Khai báo loại
Trong Go thông qua khai báo loại có thể khai báo một loại mới có tên tùy chỉnh khai báo một loại mới thông thường cần một tên loại và một loại cơ sở ví dụ đơn giản như sau
type MyInt int64Trong khai báo loại trên thông qua từ khóa type đã khai báo một loại cơ sở là int64 tên là MyInt. Trong Go mỗi loại mới khai báo đều phải có một loại cơ sở tương ứng và tên loại không khuyến nghị trùng với các định danh built-in đã có.
type MyInt int64
type MyFloat64 float64
type MyMap map[string]int
// Có thể biên dịch thành công nhưng không khuyến nghị sử dụng điều này sẽ phủ định loại gốc
type int int64Các loại được khai báo thông qua khai báo loại đều là loại mới các loại khác nhau không thể thực hiện phép toán cho dù loại cơ sở là giống nhau.
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(f1 + f)invalid operation: f1 + f (mismatched types MyFloat64 and float64)Bí danh loại
Bí danh loại khác với khai báo loại bí danh loại chỉ là một bí danh không phải tạo một loại mới ví dụ đơn giản như sau
type Int = intCả hai đều là cùng một loại chỉ khác tên gọi nên cũng có thể thực hiện phép toán do đó ví dụ dưới đây tự nhiên cũng có thể biên dịch thành công.
type Int = int
var a Int = 1
var b int = 2
fmt.Println(a + b)3Bí danh loại có công dụng rất lớn đối với một số loại đặc biệt phức tạp ví dụ hiện có một loại map[string]map[string]int đây là một map hai chiều hiện có một tham số hàm là loại map[string]map[string]int như sau
func PrintMyMap(mymap map[string]map[string]int) {
fmt.Println(mymap)
}Trong trường hợp này không cần thiết sử dụng khai báo loại vì cái trước là khai báo một loại mới không thể làm tham số của hàm đó sử dụng bí danh loại sau ví dụ như sau
type TwoDMap = map[string]map[string]int
func PrintMyMap(mymap TwoDMap) {
fmt.Println(mymap)
}Sử dụng bí danh loại sau nhìn sẽ đơn giản hơn.
TIP
Loại built-in any chính là bí danh loại của interface{} cả hai hoàn toàn tương đương chỉ khác cách gọi.
Chuyển đổi loại
Trong Go chỉ tồn tại chuyển đổi loại rõ ràng không tồn tại chuyển đổi loại ngầm do đó các biến loại khác nhau không thể thực hiện phép toán không thể làm tham số truyền. Tiền đề áp dụng của chuyển đổi loại là biết loại của biến được chuyển đổi và loại đích muốn chuyển đổi thành ví dụ như sau
type MyFloat64 float64
var f1 MyFloat64
var f float64
f1 = 0.2
f = 0.1
fmt.Println(float64(f1) + f)0.30000000000000004Thông qua chuyển đổi loại rõ ràng MyFloat64 thành loại float64 mới có thể thực hiện phép cộng. Một tiền đề khác của chuyển đổi loại là loại được chuyển đổi phải có thể được đại diện bởi loại đích (Representability) ví dụ int có thể được đại diện bởi loại int64 cũng có thể được đại diện bởi loại float64 nên giữa chúng có thể thực hiện chuyển đổi loại rõ ràng nhưng loại int không thể được đại diện bởi loại string và bool vì vậy cũng không thể thực hiện chuyển đổi loại.
TIP
Về định nghĩa của Representability có thể đến Reference Manual - Representability để biết thêm chi tiết.
Cho dù hai loại có thể đại diện cho nhau kết quả chuyển đổi loại cũng không phải luôn luôn đúng xem một ví dụ dưới đây
var num1 int8 = 1
var num2 int32 = 512
fmt.Println(int32(num1), int8(num2))1 0num1 được chuyển đổi chính xác thành loại int32 nhưng num2 thì không. Đây là một vấn đề tràn số điển hình int32 có thể biểu thị số nguyên 31 bit int8 chỉ có thể biểu thị số nguyên 7 bit số nguyên độ chính xác cao khi chuyển đổi sang số nguyên độ chính xác thấp sẽ bỏ qua bit cao giữ bit thấp do đó kết quả chuyển đổi của num1 chính là 0. Trong chuyển đổi loại số thông thường khuyến nghị chuyển nhỏ sang lớn mà không khuyến nghị chuyển lớn sang nhỏ.
Khi sử dụng chuyển đổi loại đối với một số loại cần tránh nhập nhằng ví dụ như sau
*Point(p) // tương đương với *(Point(p))
(*Point)(p) // chuyển p thành loại *Point
<-chan int(c) // tương đương với <-(chan int(c))
(<-chan int)(c) // chuyển c thành loại <-chan int
(func())(x) // chuyển x thành loại func()
(func() int)(x) // chuyển x thành loại func() intPhán đoán loại
Phán đoán loại thông thường dùng để判断 xem một biến loại interface nào đó có thuộc một loại nào đó không ví dụ như sau
var b int = 1
var a interface{} = b
if intVal, ok := a.(int); ok {
fmt.Println(intVal)
} else {
fmt.Println("error type")
}1Vì interface{} là loại interface rỗng loại interface rỗng có thể đại diện cho tất cả các loại nhưng loại int không thể đại diện cho loại interface{} nên không thể sử dụng chuyển đổi loại. Còn phán đoán loại có thể判断 xem loại底层 của nó có phải là loại muốn hay không câu lệnh phán đoán loại có hai giá trị trả về một là giá trị sau khi chuyển đổi loại hai là giá trị boolean của kết quả chuyển đổi.
Kiểm tra loại
Trong Go câu lệnh switch còn hỗ trợ một cách viết đặc biệt thông qua cách viết này có thể xử lý logic khác nhau theo các case khác nhau tiền đề sử dụng là tham số đầu vào phải là loại interface ví dụ như sau
var a interface{} = 2
switch a.(type) {
case int: fmt.Println("int")
case float64: fmt.Println("float")
case string: fmt.Println("string")
}intTIP
Thông qua các thao tác được cung cấp bởi gói unsafe có thể bỏ qua hệ thống loại của Go là có thể thực hiện các thao tác chuyển đổi loại vốn không thể biên dịch thành công.
