Trong bài trước chúng ta bàn về kiểu dữ liệu cơ bản trong Go: các kiểu số nguyên, thực, phức, kiểu luận lý và kiểu chuỗi. Bài này chúng ta bàn về kiểu dữ liệu tập hợp gồm mảng (array) và cấu trúc (struct). Các kiểu dữ liệu này được tạo ra bằng cách kết hợp các kiểu dữ liệu cơ bản theo các cách thức khác nhau.
Mảng (Array)
Mảng là một dãy liên tiếp các đối tượng một kiểu dữ liệu cụ thể. Mỗi đối tượng này gọi là phần tử mảng. Mảng cố định số phần tử (còn gọi là chiều dài mảng) sau khi tạo và Go cung cấp hàm len để lấy số phần tử này. Go cũng cung cấp toán tử a[i] để truy xuất phần tử i của mảng a với i chạy từ 0 đến len(a) - 1:
1 var a1 [2]int 2 var a2 [3]int = [3]int{1, 2, 3} 3 a3 := [3]int{4, 5} 4 fmt.Println(a1) // [0 0] 5 fmt.Println(a2) // [1 2 3] 6 fmt.Println(a3) // [4 5 0] 7 fmt.Println(len(a1)) // 2 8 fmt.Println(len(a2)) // 3 9 fmt.Println(a2[2]) // 3 10 fmt.Println(a3[2]) // 0 11 fmt.Println(a3[3]) // Lỗi khi biên dịch
- Dòng 1 khai báo một mảng số nguyên a1 với 2 phần tử. Mặc định Go sẽ gán giá trị 0 cho từng phần tử của mảng này.
- Dòng 2 thì mảng số nguyên a2 được khai báo với 3 phần tử kèm theo giá trị khởi tạo cho các phần tử này. Go hỗ trợ cách khai báo khác cho kết quả tương tự a2 := [...]int{1, 2, 3}. Cách khai báo này phù hợp khi khai báo mảng có số phần tử lớn. Lúc này số phần tử của mảng sẽ được xác định dựa trên số lượng giá trị khai báo.
- Dòng 3 là cách khai báo nhanh mảng nguyên a3 có 3 phần tử và khởi tạo giá trị cho 2 phần tử, Go sẽ gán 0 cho phần tử còn lại. Go hỗ trợ cách khai báo tương tự là a3 := [3]int{0:4,1:5}. Cách này thuận tiện nếu chỉ muốn khai báo giá trị vài phần tử không liên tiếp trong mảng. Nếu khai báo a3 := [...]int{0:4,1:5} thì mảng a3 được tạo chỉ có 2 phần tử vì Go căn cứ trên chỉ số khai báo cao nhất để ấn định số phần tử mảng.
- Dòng 4,5 và 6 in thông tin mảng a1, a2, a3 lên màn hình.
- Dòng 7 và 8 in chiều dài mảng a1 và a2.
- Dòng 9 và 10 in phần tử thứ 2 của a2 và a3.
- Dong 11 dự định in phần tử thứ 3 của a3 nhưng rất tiếc Go sẽ báo lỗi khi biên dịch do a3 chỉ có 3 phần tử nên chỉ được phép truy xuất tối đa đến phần tử 2 mà thôi vì mảng bắt đầu từ phần tử 0.
Chiều dài mảng là một thuộc tính của kiểu dữ liệu mảng nên việc gán a1 = a2 với a1, a2 ở ví dụ trên là không được phép. Go sẽ báo lỗi khi biên dịch. Nhưng a3 = a2 thì được. Lúc này in mảng a3 ra sẽ có kết quả [0 0 0].
Nếu kiểu dữ liệu phần tử của mảng hỗ trợ so sánh thì chúng ta có thể so sánh mảng với nhau qua 2 phép so sánh bằng = và khác !=. Ví dụ:
- Dòng 4 và 5 so sánh bằng 2 mảng với nhau. Cả 2 phép so sánh đều cho kết quả là sai.
- Dòng 6 gán giá trị mới cho phần tử 2 thuộc mảng a3. Lúc này giá trị của mảng a3 [1 2 3] hoàn toàn giống giá trị mảng a2.
- Dòng 7 so sánh bằng giữa a3 và a2, kết quả nhận được là đúng (true).
- Dòng 8 và 9 so sánh khác: dòng 8 cho kết quả đúng (true) do a1 khác a2 nhưng dòng 9 cho kết quả false do a2 bằng a3.
Mảng ít khi được dùng trong lập trình Go do ràng buộc cố định chiều dài nhưng mảng đóng vai trò quan trọng như là đối tượng lưu giữ dữ liệu cho kiểu dữ liệu slice (lát) mà chúng ta sẽ tìm hiểu sau.
- Dòng 2 thì mảng số nguyên a2 được khai báo với 3 phần tử kèm theo giá trị khởi tạo cho các phần tử này. Go hỗ trợ cách khai báo khác cho kết quả tương tự a2 := [...]int{1, 2, 3}. Cách khai báo này phù hợp khi khai báo mảng có số phần tử lớn. Lúc này số phần tử của mảng sẽ được xác định dựa trên số lượng giá trị khai báo.
- Dòng 3 là cách khai báo nhanh mảng nguyên a3 có 3 phần tử và khởi tạo giá trị cho 2 phần tử, Go sẽ gán 0 cho phần tử còn lại. Go hỗ trợ cách khai báo tương tự là a3 := [3]int{0:4,1:5}. Cách này thuận tiện nếu chỉ muốn khai báo giá trị vài phần tử không liên tiếp trong mảng. Nếu khai báo a3 := [...]int{0:4,1:5} thì mảng a3 được tạo chỉ có 2 phần tử vì Go căn cứ trên chỉ số khai báo cao nhất để ấn định số phần tử mảng.
- Dòng 4,5 và 6 in thông tin mảng a1, a2, a3 lên màn hình.
- Dòng 7 và 8 in chiều dài mảng a1 và a2.
- Dòng 9 và 10 in phần tử thứ 2 của a2 và a3.
- Dong 11 dự định in phần tử thứ 3 của a3 nhưng rất tiếc Go sẽ báo lỗi khi biên dịch do a3 chỉ có 3 phần tử nên chỉ được phép truy xuất tối đa đến phần tử 2 mà thôi vì mảng bắt đầu từ phần tử 0.
Chiều dài mảng là một thuộc tính của kiểu dữ liệu mảng nên việc gán a1 = a2 với a1, a2 ở ví dụ trên là không được phép. Go sẽ báo lỗi khi biên dịch. Nhưng a3 = a2 thì được. Lúc này in mảng a3 ra sẽ có kết quả [0 0 0].
Nếu kiểu dữ liệu phần tử của mảng hỗ trợ so sánh thì chúng ta có thể so sánh mảng với nhau qua 2 phép so sánh bằng = và khác !=. Ví dụ:
- 3 dòng đầu tiên khai báo 3 mảng 3 phần tử. Giá trị của các mảng là a1 [0 0 0], a2 [1 2 3], a3 [1 2 0]var a1 [3]int var a2 [3]int = [3]int{1, 2, 3} a3 := [3]int{1, 2} fmt.Println(a1 == a2) // false fmt.Println(a2 == a3) // false a3[2] = 3 fmt.Println(a3 == a2) // true fmt.Println(a1 != a2) // true fmt.Println(a2 != a3) // false
- Dòng 4 và 5 so sánh bằng 2 mảng với nhau. Cả 2 phép so sánh đều cho kết quả là sai.
- Dòng 6 gán giá trị mới cho phần tử 2 thuộc mảng a3. Lúc này giá trị của mảng a3 [1 2 3] hoàn toàn giống giá trị mảng a2.
- Dòng 7 so sánh bằng giữa a3 và a2, kết quả nhận được là đúng (true).
- Dòng 8 và 9 so sánh khác: dòng 8 cho kết quả đúng (true) do a1 khác a2 nhưng dòng 9 cho kết quả false do a2 bằng a3.
Mảng ít khi được dùng trong lập trình Go do ràng buộc cố định chiều dài nhưng mảng đóng vai trò quan trọng như là đối tượng lưu giữ dữ liệu cho kiểu dữ liệu slice (lát) mà chúng ta sẽ tìm hiểu sau.
Cấu trúc (struct)
Cấu trúc là một kiểu dữ liệu tập hợp bao gồm nhóm nhiều đối tượng có kiểu dữ liệu khác nhau. Mỗi đối tượng gọi là 1 trường (field). Vì cấu trúc gồm nhiều trường do chúng ta tự chọn nên mỗi kiểu dữ liệu cấu trúc cụ thể cần phải được định nghĩa chi tiết như sau:
Tên trường có chữ cái đầu viết hoa sẽ cho phép bên ngoài thấy được trường này và truy xuất trường đó qua toán tử dấu chấm . . Chúng ta cần chú ý đặc điểm này để tránh trường hợp không truy xuất hay gán dữ liệu cho trường được mà không rõ tại sao. Tôi đã từng bị khi khai báo nhanh một cấu trúc ứng với json để phân tích dữ liệu từ json nhưng dữ liệu rỗng do các trường có tên bắt đầu chữ cái thường do đặt theo cấu trúc json. Vấn đề này sẽ nói rõ bên dưới khi bàn về json.
Go không cho phép kiểu dữ liệu 1 trường lại chính là cấu trúc mà trường đó thuộc về. Nói cách khác, việc khai báo trường F trong cấu trúc S có kiểu dữ liệu chính là S là không hợp lệ. Tuy nhiên là con trỏ S thì hợp lệ để thuận tiện trong việc tạo ra xâu liên kết cấu trúc.
Cấu trúc không có trường nào gọi là cấu trúc rỗng. Mỗi trường sẽ được khai báo trên mỗi dòng nhưng các trường cùng kiểu dữ liệu có thể gộp khai báo chung trên 1 dòng. Tuy nhiên chúng ta có thể sử dụng cách khai báo biến cấu trúc nhanh dựa trên thứ tự trường nên việc khai báo trường gộp như vậy là không nên, trừ khi chúng liên quan mật thiết nhau.type <tên cấu trúc> struct {
<tên trường thứ nhất> <kiểu trường thứ nhất>
...
<tên trường thứ n> <kiểu trường thứ n>
}
Tên trường có chữ cái đầu viết hoa sẽ cho phép bên ngoài thấy được trường này và truy xuất trường đó qua toán tử dấu chấm . . Chúng ta cần chú ý đặc điểm này để tránh trường hợp không truy xuất hay gán dữ liệu cho trường được mà không rõ tại sao. Tôi đã từng bị khi khai báo nhanh một cấu trúc ứng với json để phân tích dữ liệu từ json nhưng dữ liệu rỗng do các trường có tên bắt đầu chữ cái thường do đặt theo cấu trúc json. Vấn đề này sẽ nói rõ bên dưới khi bàn về json.
Go không cho phép kiểu dữ liệu 1 trường lại chính là cấu trúc mà trường đó thuộc về. Nói cách khác, việc khai báo trường F trong cấu trúc S có kiểu dữ liệu chính là S là không hợp lệ. Tuy nhiên là con trỏ S thì hợp lệ để thuận tiện trong việc tạo ra xâu liên kết cấu trúc.
Ví dụ sau mô tả cấu trúc sinh viên đơn giản với 3 trường: mã sinh viên, tên và tuổi:
- Dòng 7 khai báo một cấu trúc Student cho biến studentA nhưng chưa gán giá trị. Kết quả Go tạo giá trị mặc định zero như thấy ở dòng 8.
- Dòng 9-11 thiết lập giá trị cho các trường của studentA. Chúng ta thấy để sử dụng các trường chúng ta dùng cú pháp <tên biến cấu trúc>.<tên trường cần truy xuất>
- Dòng 12-14 khai báo có khởi tạo giá trị cho 3 biến cấu trúc Student là studentB, studentC và studentD với các cách thức khác nhau. Dòng 12 và 13 là cách khai báo chuẩn với giá trị được gán đi kèm trường tương ứng. Khi không gán, Go sẽ gán giá trị mặc định là zero như trường studentC.Age. Dòng 14 là cách khai báo nhanh và gán giá trị dựa trên thứ tự khai báo. Cẩn thận với cách khai báo này vì có thể gán sai giá trị.
- Dòng 15-18 in các biến kiểu cấu trúc Student lên màn hình.
Bài tiếp theo sẽ tìm hiểu về kiểu dữ liệu tham chiếu là con trỏ.
- Dòng 1-5: Khai báo kiểu dữ liệu cấu trúc Student với 3 trường ID (int), Name (string) và Age (int) ứng với mã, tên và tuổi.1 type Student struct { 2 ID int 3 Name string 4 Age int 5 } 6 7 var studentA Student 8 fmt.Println(studentA) // {0 "" 0} 9 studentA.ID = 1 10 studentA.Name = "Mai" 11 studentA.Age = 20 12 var studentB = Student{ID: 2, Name: "Lan", Age: 18} 13 var studentC = Student{ID: 3, Name: "Cúc"} 14 studentD := Student{4, "Trúc", 16} 15 fmt.Println(studentA) // {1 Mai 20} 16 fmt.Println(studentB) // {2 Lan 18} 17 fmt.Println(studentC) // {3 Cúc 0} 18 fmt.Println(studentD) // {4 Trúc 16}
- Dòng 7 khai báo một cấu trúc Student cho biến studentA nhưng chưa gán giá trị. Kết quả Go tạo giá trị mặc định zero như thấy ở dòng 8.
- Dòng 9-11 thiết lập giá trị cho các trường của studentA. Chúng ta thấy để sử dụng các trường chúng ta dùng cú pháp <tên biến cấu trúc>.<tên trường cần truy xuất>
- Dòng 12-14 khai báo có khởi tạo giá trị cho 3 biến cấu trúc Student là studentB, studentC và studentD với các cách thức khác nhau. Dòng 12 và 13 là cách khai báo chuẩn với giá trị được gán đi kèm trường tương ứng. Khi không gán, Go sẽ gán giá trị mặc định là zero như trường studentC.Age. Dòng 14 là cách khai báo nhanh và gán giá trị dựa trên thứ tự khai báo. Cẩn thận với cách khai báo này vì có thể gán sai giá trị.
- Dòng 15-18 in các biến kiểu cấu trúc Student lên màn hình.
Bài tiếp theo sẽ tìm hiểu về kiểu dữ liệu tham chiếu là con trỏ.
Tóm tắt:
- Mảng là dãy liên tiếp các đối tượng cùng kiểu dữ liệu:
+ Khai báo: var <tên biến> [<số phần tử>]<kiểu dữ liệu phần tử>
+ Mỗi đối tượng được gọi là phần tử. Hàm len(a) cho chiều dài mảng chính là số phần tử của mảng a và a[i] cho giá trị phần tử i của mảng a.
+ Mảng không thay đổi chiều dài được.
+ Mảng chủ yếu dùng để lưu trữ dữ liệu cho kiểu slice.
- Cấu trúc là tập hợp các đối tượng có kiểu dữ liệu khác nhau:
+ Mỗi đối tượng gọi là trường của cấu trúc.
+ Khai báo: type <tên kiểu dữ liệu> struct {<tên trường> <tên kiểu dữ liệu>}
+ Kiểu dữ liệu 1 trường chính là cấu trúc đó là bất hợp lệ, sử dụng con trỏ của cấu trúc đó thì được.
Đoạn cuối này em ko hiểu cho lắm: "Kiểu dữ liệu 1 trường chính là cấu trúc đó là bất hợp lệ, sử dụng con trỏ của cấu trúc đó thì được" .. anh có thể giải thích rõ hơn được không ạ
ReplyDeleteThêm chữ "có" sau chữ "Kiểu dữ liệu" thì chắc sẽ rõ ràng hơn. Nhưng ngay câu sau có giải thích đó bạn. Đại ý là nếu khai báo:
ReplyDeletetype S struct {
ID int
Name string
Age int
F S
}
là không được, nhưng thay F S thành F *S thì lại hợp lệ.
Mình nhận thấy cũng khá giống với C
ReplyDelete"Tên trường có chữ cái đầu viết hoa sẽ cho phép bên ngoài thấy được trường này và truy xuất trường đó qua toán tử dấu chấm ."
ReplyDeleteSao em thử đặt tên trường là id thay vì ID thì vẫn truy xuất đc studentA.id anh nhỉ?
Bên ngoài ở đây là bên ngoài package đó bạn. Do đó cùng 1 package thì bạn dùng bình thường thôi nhưng khi sử dụng ở package khác sẽ không được.
DeleteOk, cảm ơn anh nhiều.
Delete