Trong các ngôn ngữ lập trình hướng đối tượng (OOP) truyền thống như Java hay C++, mọi thứ thường được gói gọn chặt chẽ bên trong các lớp (class). Go lại chọn một triết lý tối giản và linh hoạt hơn: phân tách hoàn toàn giữa dữ liệu (type) và hành vi (func), sau đó kết nối chúng lại khi cần thiết.
Bài viết này sẽ giúp bạn nắm bắt cốt lõi của hai từ khóa quan trọng này.
1. Từ khóa type — Định nghĩa cấu trúc dữ liệu
Go là một ngôn ngữ định kiểu tĩnh (statically typed) rất nghiêm ngặt. Từ khóa type được dùng để định nghĩa hoặc tạo bí danh cho các kiểu dữ liệu, giúp mã nguồn an toàn và dễ đọc hơn.
Các nhóm kiểu dữ liệu mặc định
Trước khi tạo kiểu mới, bạn cần biết Go có sẵn các nhóm kiểu cơ bản:
- Kiểu cơ bản (Basic types): Số nguyên (
int,uint…), số thực (float64…),bool,string,rune. - Kiểu cấu trúc (Composite types):
array,slice(mảng động),map(bảng băm). - Kiểu giao tiếp (Interface types):
interface.
Type Definition vs Type Alias
Bạn có thể tự định nghĩa một kiểu dữ liệu mới hoàn toàn dựa trên kiểu có sẵn. Có hai cách tiếp cận với sự khác biệt tinh tế:
Type Definition (Định nghĩa kiểu mới):
1 2 3 4 5 6// Tạo một kiểu mới tên là UserID, dựa trên kiểu int. type UserID int var id UserID = 10 // var num int = id // Lỗi biên dịch: Không thể gán vì UserID và int là 2 kiểu khác biệt var num int = int(id) // Hợp lệ: Cần ép kiểu tường minhUserIDlà một kiểu hoàn toàn mới. Dù nó dựa trênint, bạn không thể gán trực tiếp một biếnintchoUserIDmà không ép kiểu tường minh. Điều này giúp ngăn chặn các lỗi gán nhầm ID.Type Alias (Bí danh):
1 2 3 4 5// Tạo bí danh MyString cho kiểu string. type MyString = string var name MyString = "Golang" var str string = name // Hợp lệ: Gán trực tiếp thoải mái mà không cần ép kiểuChỉ định tên gọi khác. Dùng dấu
=có nghĩaMyStringvàstringlà một, bạn có thể gán qua lại thoải mái.
Kiểu cấu trúc (Struct)
Go không có class. Để gom nhóm các dữ liệu liên quan, Go sử dụng struct.
| |
Go cũng không có khái niệm “kế thừa” (inheritance) bằng từ khóa extends. Thay vào đó, Go dùng cơ chế tổ hợp (composition) bằng cách nhúng các struct vào nhau (Embedded fields / Anonymous fields).
2. Từ khóa func — Định nghĩa hành vi
Hàm (func) trong Go là “công dân hạng nhất” (First-Class Citizens). Bạn có thể gán hàm cho biến, truyền hàm làm tham số, hoặc trả về một hàm từ một hàm khác.
Cú pháp khai báo chuẩn
Một hàm cơ bản nhận vào các tham số và trả về kết quả. Nếu các tham số liền kề cùng kiểu, bạn có thể viết gộp:
| |
Trả về nhiều giá trị (Multiple Return Values)
Điểm khác biệt cốt lõi của hàm trong Go là khả năng trả về đa giá trị cùng lúc (thay vì phải gói vào struct hay mảng). Tính năng này cung cấp nền tảng vững chắc cho triết lý “Error as Values”. Thay vì sử dụng cơ chế ném ngoại lệ (Exception) gây mất kiểm soát và giảm hiệu năng khi unwinding stack, Go khuyến khích xử lý lỗi một cách tường minh ngay tại luồng gọi.
| |
Truyền tham số: Luôn là Pass-by-Value
Mọi tham số truyền vào hàm trong Go đều được sao chép giá trị (Pass-by-Value). Nếu bạn thay đổi biến trong hàm, biến gốc bên ngoài không bị ảnh hưởng. Muốn thay đổi biến gốc? Bạn phải truyền vào một con trỏ (pointer).
| |
Hàm ẩn danh & defer
- Hàm ẩn danh (Anonymous Function) & Closures: Bạn có thể viết các hàm không cần tên, thường để chạy ngay lập tức hoặc gán vào biến. Hàm này có khả năng “ghi nhớ” môi trường xung quanh (closure).
1 2 3 4 5 6 7 8// Closure: Hàm ghi nhớ biến 'count' ở bên ngoài count := 0 increment := func() int { count++ return count } fmt.Println(increment()) // In ra 1 fmt.Println(increment()) // In ra 2 - Từ khóa
defer: Giúp trì hoãn việc thực thi một lệnh (thường là lệnh đóng file, unlock bộ nhớ) cho đến khi hàm bao quanh nó kết thúc.
3. Sự kết hợp hoàn hảo: Phương thức (Methods)
Làm thế nào để tạo ra một đối tượng vừa mang dữ liệu (struct), vừa có hành vi (func) nếu Go không có class?
Câu trả lời là Phương thức (Methods). Bạn có thể gắn một func vào một type bất kỳ (do bạn tự định nghĩa) bằng cách sử dụng một tham số đặc biệt gọi là Receiver (Bộ tiếp nhận) đặt ở giữa func và tên hàm.
| |
Lưu ý quan trọng về Receiver:
- Value Receiver:
(r Rectangle)- Hàm nhận được một bản sao của struct. Mọi thay đổi trong hàm không tác động đến struct gốc. - Pointer Receiver:
(r *Rectangle)- Hàm nhận được con trỏ. Dùng khi hàm cần thay đổi dữ liệu gốc của struct, hoặc khi struct quá lớn, tránh việc sao chép làm tốn bộ nhớ.
4. Giao tiếp qua Interface
Khi đã hiểu type và func, bạn sẽ dễ dàng làm chủ interface. Interface trong Go định nghĩa một tập hợp các chữ ký của hàm (hành vi).
Khác biệt lớn nhất của Go là Implicit Implementation (triết lý Structural Typing/Duck Typing). Nếu một type (như Rectangle ở trên) định nghĩa đủ tất cả các phương thức mà một interface yêu cầu, nó tự động thỏa mãn (implement) interface đó mà không cần phải dùng từ khóa implements.
| |
Và khi bạn gặp interface{} (hay any từ phiên bản Go 1.18), đây là một empty interface (interface rỗng), không yêu cầu phương thức nào cả. Do đó, mọi kiểu dữ liệu trong Go đều tự động thỏa mãn interface{}.
Tóm tắt cốt lõi:
- Dùng
type(đặc biệt làstruct) để định nghĩa xem “đối tượng chứa dữ liệu gì”.- Dùng
funckết hợp với Receiver để quy định “đối tượng đó làm được gì”.- Go chuộng sự kết hợp (Composition) qua nhúng struct thay vì kế thừa (Inheritance) lằng nhằng.
Sự tách bạch và rõ ràng này chính là bí quyết giúp mã nguồn Go luôn gọn gàng, dễ đọc, dễ bảo trì và vô cùng linh hoạt.