Đi tới nội dung chính
Go

Bài 1.3: Kiểu Dữ Liệu và Hàm (Type & Func)

Khám phá hai khối xây dựng cốt lõi trong Go: từ khóa type dùng để định nghĩa dữ liệu và func để định nghĩa hành vi, cùng cách chúng kết hợp tạo nên sự linh hoạt.

6 phút1277 từMạnh Cường

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 minh
    

    UserID là một kiểu hoàn toàn mới. Dù nó dựa trên int, bạn không thể gán trực tiếp một biến int cho UserID mà 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ểu
    

    Chỉ định tên gọi khác. Dùng dấu = có nghĩa MyStringstring là 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.

1
2
3
4
5
type User struct {
    ID    UserID
    Name  string
    Email string
}

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:

1
2
3
func multiply(x, y int) int {
    return x * y
}

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.

1
2
3
4
5
6
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("cannot divide by zero")
    }
    return a / b, nil
}

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).

1
2
3
func updateAge(age *int) {
    *age = 30 // Thay đổi trực tiếp giá trị tại vùng nhớ của biến gốc
}

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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
type Rectangle struct {
    Width  float64
    Height float64
}

// Hàm Area được gắn vào kiểu Rectangle (r là Value Receiver)
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Sử dụng:
rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area()) // 50

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 typefunc, 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.

1
2
3
4
// Bất kỳ kiểu nào có hàm Area() trả về float64 đều là một Shape
type Shape interface {
    Area() float64 
}

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 func kế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.

Chủ đề của bài viết

Chia sẻ bài viết

Gửi bài này cho người đang cần đúng chủ đề, hoặc lưu lại để quay về sau.

Đọc tiếp

Tất cả lộ trình học

Tiếp theo

Gợi ý bài tiếp theo

Bài 1.1d: Access Points (Điểm truy cập không dây)

Access Point (AP) là thành phần đưa thiết bị Wi-Fi vào mạng LAN có dây. Nếu router chịu trách nhiệm định tuyến giữa các mạng và switch chịu trách nhiệm chuyển …

Mở bài tiếp theo