Tìm Hiểu Cơ Bản về Ngôn Ngữ Lập Trình Go

Bài viết này sẽ hướng dẫn bạn những kiến thức nền tảng về ngôn ngữ lập trình Go (Golang), từ cài đặt môi trường, cấu trúc cơ bản, đến các tính năng cốt lõi và cách làm việc với chúng.

Cài đặt Go

Để bắt đầu với Go, bạn cần cài đặt Go SDK trên hệ thống của mình:

  1. Tru cập trang tải xuống chính thức: https://golang.org/dl/
  2. Chọn gói cài đặt phù hợp với hệ điều hành của bạn (Windows, macOS, Linux).
  3. Thực hiện cài đặt:

    • Windows/macOS: Chạy file cài đặt đã tải về và làm theo hướng dẫn.
    • Linux: Giải nén gói đã tải về vào thư mục mong muốn (ví dụ: /usr/local).
  4. Thiết lập biến môi trường (chỉ cần thiết cho Linux, Windows/macOS thường tự động):

    Thêm các dòng sau vào file cấu hình shell của bạn (ví dụ: ~/.bashrc hoặc ~/.zshrc), sau đó chạy source ~/.bashrc:

    export GOROOT=/path/to/go # Ví dụ: /usr/local/go
    export PATH=$PATH:$GOROOT/bin
    export GOPATH=$HOME/go_projects # Thư mục làm việc của bạn cho các dự án Go
    export PATH=$PATH:$GOPATH/bin

    GOROOT trỏ đến thư mục cài đặt Go. GOPATH là nơi Go sẽ tìm các mã nguồn, gói và các file thực thi của bạn. PATH bao gồm các thư mục bin của GOROOTGOPATH để bạn có thể chạy các lệnh Go từ bất kỳ đâu.

Thiết lập Môi trường Phát triển

Để có trải nghiệm phát triển tốt nhất với Go, chúng ta sẽ sử dụng Visual Studio Code (VS Code).

  1. Cài đặt VS Code:

  2. Cài đặt Go Extension cho VS Code:

    • Mở VS Code.
    • Vào mục Extensions (Ctrl+Shift+X hoặc Cmd+Shift+X).
    • Tìm kiếm "Go" và cài đặt extension do Go Team phát triển.
  3. Tạo dự án đầu tiên:

    • Tạo một thư mục mới cho dự án của bạn, ví dụ: $GOPATH/src/my_first_app.
    • Mở thư mục này trong VS Code (File > Open Folder).
    • Trong VS Code, chuột phải vào thư mục và chọn New File, đặt tên là main.go.
    • Khi bạn lưu file, VS Code có thể sẽ gợi ý cài đặt các công cụ Go cần thiết (như gopls, dlv, v.v.). Hãy nhấp vào Install All.
  4. Cài đặt công cụ Debugger (Delve):

    Delve là trình gỡ lỗi mạnh mẽ cho Go. VS Code sử dụng Delve để debug.

    • Mở terminal trong VS Code (Ctrl+` hoặc Cmd+`).
    • Chạy lệnh sau để cài đặt Delve:
    • go install github.com/go-delve/delve/cmd/dlv@latest

Các Tính năng Đặc trưng của Ngôn ngữ Go

1. Thu gom rác tự động (Garbage Collection)

  • Go tự động quản lý bộ nhớ, giải phóng lập trình viên khỏi gánh nặng quản lý thủ công như trong C++.
  • Điều này giúp bạn tập trung vào logic nghiệp vụ và giảm thiểu lỗi liên quan đến bộ nhớ.
  • Chỉ cần cấp phát bộ nhớ bằng new() hoặc make(), không cần giải phóng.

2. Hỗ trợ lập trình Concurrency tự nhiên

  • Go được thiết kế với khả năng xử lý đồng thời (concurrency) ngay từ cấp độ ngôn ngữ.
  • Goroutines: Là các "tiến trình nhẹ" (lightweight threads) của Go. Bạn có thể tạo hàng nghìn, thậm chí hàng triệu goroutine một cách hiệu quả.
  • Go sử dụng mô hình CSP (Communicating Sequential Processes) để quản lý concurrency.
  • Ví dụ tạo một goroutine:
    package main
    
    import "fmt"
    
    func main() {
        go fmt.Println("Xin chào từ một Goroutine!")
        fmt.Println("Xin chào từ Main Goroutine!")
        // Để thấy output từ goroutine, bạn có thể cần một chút độ trễ hoặc cơ chế đồng bộ hóa
        // Ví dụ đơn giản này có thể kết thúc trước khi goroutine kia kịp chạy.
    }

3. Kênh (Channels)

  • Channels là các "đường ống" an toàn để các goroutine giao tiếp với nhau, tương tự như các đường ống (pipes) trong Unix/Linux.
  • Chúng hỗ trợ truyền dữ liệu thuộc bất kỳ kiểu nào.
  • Ví dụ sử dụng channel:
    package main
    
    import "fmt"
    
    func main() {
        dataChannel := make(chan int, 3) // Tạo một channel với bộ đệm 3 phần tử
        dataChannel <- 10                // Gửi giá trị 10 vào channel
        dataChannel <- 20                // Gửi giá trị 20 vào channel
        
        receivedVal := <-dataChannel     // Nhận giá trị từ channel
        fmt.Println("Giá trị nhận được:", receivedVal) // Output: Giá trị nhận được: 10
    }

4. Đa giá trị trả về

  • Một hàm trong Go có thể trả về nhiều giá trị. Điều này rất hữu ích cho việc trả về kết quả và lỗi cùng lúc.
  • Ví dụ hàm trả về đa giá trị:
    package main
    
    import "fmt"
    
    func calculateStats(val1, val2 int) (int, float64) {
        sumResult := val1 + val2
        averageResult := float64(val1+val2) / 2.0
        return sumResult, averageResult
    }
    
    func main() {
        total, average := calculateStats(5, 7)
        fmt.Printf("Tổng: %d, Trung bình: %.2f\n", total, average) // Output: Tổng: 12, Trung bình: 6.00
    }

Chương trình "Hello, World!"

Dưới đây là chương trình "Hello, World!" kinh điển trong Go. Hãy tạo file main.go trong thư mục dự án của bạn và dán nội dung sau:

package main // Khai báo gói 'main', đây là gói bắt buộc cho các chương trình thực thi

import (
    "fmt" // Import gói 'fmt' để định dạng và in output
)

func main() { // Hàm 'main' là điểm khởi đầu của mọi chương trình Go
    fmt.Println("Chào thế giới từ Go!") // In chuỗi "Chào thế giới từ Go!" ra console
}

Để chạy chương trình, mở terminal trong thư mục my_first_app và gõ go run main.go.

Khái niệm về Gói (Packages)

  • Trong Go, mã nguồn được tổ chức thành các gói, tương tự như module trong Python. Các gói cùng chức năng thường được đặt trong cùng một thư mục.
  • Gói main: Là gói đặc biệt dùng để tạo ra các chương trình thực thi. Mỗi ứng dụng Go chỉ có duy nhất một gói main và phải chứa hàm main().
  • Mục đích chính của gói là tăng khả năng tái sử dụng mã.

Ví dụ sử dụng gói tùy chỉnh

1. Trong thư mục my_first_app, tạo một thư mục mới tên là math_ops.

2. Trong thư mục math_ops, tạo file arithmetic.go với nội dung sau:

package math_ops // Khai báo gói 'math_ops'

// Add thực hiện phép cộng hai số nguyên và trả về kết quả
func Add(a, b int) int { // Tên hàm bắt đầu bằng chữ hoa để có thể xuất khẩu ra ngoài gói
    return a + b
}

// Subtract thực hiện phép trừ hai số nguyên
func Subtract(a, b int) int {
    return a - b
}

3. Sửa đổi file main.go của bạn để sử dụng gói math_ops:

package main

import (
    "fmt"
    "my_first_app/math_ops" // Import gói math_ops. Đường dẫn dựa trên GOPATH/src
)

func main() {
    num1 := 10
    num2 := 5
    
    // Gọi hàm Add từ gói math_ops
    sumResult := math_ops.Add(num1, num2)
    fmt.Printf("Tổng của %d và %d là: %d\n", num1, num2, sumResult) // Output: Tổng của 10 và 5 là: 15

    // Gọi hàm Subtract
    diffResult := math_ops.Subtract(num1, num2)
    fmt.Printf("Hiệu của %d và %d là: %d\n", num1, num2, diffResult) // Output: Hiệu của 10 và 5 là: 5
}

Để chạy chương trình này, bạn cần đảm bảo rằng thư mục my_first_app được khởi tạo module Go. Mở terminal tại thư mục my_first_app và chạy:

go mod init my_first_app
go run main.go

Kiểu dữ liệu cơ bản và Toán tử

Quy tắc đặt tên file, từ khóa và định danh

  • Tất cả các file mã nguồn Go đều có phần mở rộng .go.
  • Định danh (Identifiers): Phải bắt đầu bằng một chữ cái hoặc dấu gạch dưới (_). Go phân biệt chữ hoa, chữ thường. Ví dụ: myVariableMyVariable là khác nhau.
  • _ (dấu gạch dưới) là một định danh đặc biệt, dùng để bỏ qua một giá trị (ví dụ: bỏ qua giá trị trả về của một hàm).
  • Go có các từ khóa dành riêng không thể dùng làm định danh (ví dụ: func, var, package, import, if, for, v.v.).

Cấu trúc cơ bản của một chương trình Go

package main // Khai báo gói

import (
	"fmt" // Nhập các gói cần thiết
)

func main() { // Hàm main, điểm bắt đầu thực thi
	fmt.Println("Chào các bạn!")
}
  • Mọi file mã nguồn Go đều phải thuộc về một gói.
  • Từ khóa import được dùng để đưa các gói khác vào chương trình.
  • Một chương trình thực thi Go phải thuộc gói main và có một hàm main() duy nhất.
  • Gọi hàm:
    • Hàm trong cùng gói: Gọi trực tiếp (ví dụ: fmt.Println() nếu bạn tự định nghĩa hàm Println trong gói fmt).
    • Hàm từ gói khác: Gọi thông qua tên gói và tên hàm (ví dụ: fmt.Println(), math_ops.Add()).
  • Quy tắc kiểm soát truy cập gói:
    • Định danh (hàm, biến, kiểu) bắt đầu bằng chữ hoacó thể xuất khẩu (exported), tức là có thể truy cập từ bên ngoài gói.
    • Định danh bắt đầu bằng chữ thườngriêng tư (unexported), chỉ có thể truy cập bên trong gói đó.

Khai báo Hàm

Cú pháp cơ bản của khai báo hàm:

func TenHam(danhSachThamSo) (danhSachGiaTriTraVe) {
    // Thân hàm
    return ...
}
  • Ví dụ:
    // Hàm không có tham số và không có giá trị trả về
    func sayHello() {
        // ...
    }
    
    // Hàm với hai tham số kiểu int, không có giá trị trả về
    func addNumbers(x int, y int) {
        // ...
    }
    
    // Hàm với hai tham số kiểu int và trả về hai giá trị kiểu int
    func performOperation(x int, y int) (int, int) {
        // ...
        return x + y, x * y
    }

Hằng số (Constants)

  • Hằng số được khai báo với từ khóa const, giá trị của chúng không thể thay đổi sau khi khởi tạo.
  • Chỉ có thể khai báo hằng số cho các kiểu dữ liệu boolean, number (int, float, complex) và string.
  • Cú pháp: const identifier [type] = value. Kiểu dữ liệu (type) có thể bỏ qua nếu trình biên dịch có thể suy luận.
  • Ví dụ:
    const appName string = "MyGoApp"
    const pi = 3.14159 // Trình biên dịch suy luận kiểu là float64
    const (
        statusOK    = 200
        statusError = 500
        maxAttempts = 5
    )

Biến (Variables)

  • Cú pháp khai báo biến: var identifier type
  • Ví dụ:
    var counter int
    var message string
    
    // Khai báo và khởi tạo
    var initialValue int = 100
    var greeting string = "Hello Go"
    
    // Khai báo ngắn gọn (chỉ dùng trong hàm)
    shortVar := "Đây là một chuỗi"
    anotherNum := 42
    
    // Khai báo nhiều biến cùng lúc
    var (
        userCount int
        userName  string
        isActive  bool
        version   float32 = 1.2
    )

Kiểu giá trị và Kiểu tham chiếu

  • Kiểu giá trị (Value Types): Biến lưu trữ trực tiếp giá trị. Bộ nhớ thường được cấp phát trên stack. Khi gán hoặc truyền vào hàm, giá trị được sao chép. Các kiểu bao gồm: int, float, bool, string, arraystruct (nếu không chứa con trỏ).
  • Kiểu tham chiếu (Reference Types): Biến lưu trữ một địa chỉ bộ nhớ, nơi giá trị thực sự được đặt. Bộ nhớ thường được cấp phát trên heap và được GC quản lý. Khi gán hoặc truyền vào hàm, địa chỉ được sao chép. Các kiểu bao gồm: pointers, slices, maps, channelsfunctions.

Phạm vi (Scope) của Biến

  • Biến khai báo trong hàm được gọi là biến cục bộ (local variable), phạm vi sống giới hạn trong hàm đó.
  • Biến khai báo ngoài hàm được gọi là biến toàn cục (global variable). Phạm vi sống là toàn bộ gói. Nếu biến toàn cục bắt đầu bằng chữ hoa, nó có thể truy cập từ toàn bộ chương trình (từ các gói khác).

Kiểu dữ liệu và Toán tử (tiếp)

Kiểu Boolean (bool)

  • Chỉ có thể chứa true hoặc false.
  • Ví dụ:
    var isValid bool // Mặc định là false
    var isActive bool = true
    isCompleted := false
  • Toán tử liên quan: ! (NOT), && (AND), || (OR).

Kiểu số (Numeric Types)

  • Go cung cấp nhiều kiểu số nguyên (int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64) và số thực dấu phẩy động (float32, float64).
  • Chuyển đổi kiểu: Phải thực hiện tường minh bằng cú pháp TargetType(variable).
  • Ví dụ:
    package main
    
    import "fmt"
    
    func main() {
    	var valInt int = 10
    	var valInt32 int32
    
    	valInt32 = int32(valInt + 5) // Chuyển đổi int sang int32
    	fmt.Println("Giá trị int32:", valInt32) // Output: Giá trị int32: 15
    }

Kiểu ký tự (Character) và Chuỗi (String)

  • Ký tự: Go sử dụng kiểu byte (tên khác của uint8) để biểu diễn một byte đơn hoặc rune (tên khác của int32) để biểu diễn một ký tự Unicode.
  • Ví dụ:
    var charByte byte = 'A' // Biểu diễn mã ASCII của 'A'
    var charRune rune = '你好' // Biểu diễn ký tự Unicode
  • Chuỗi: Kiểu string dùng để biểu diễn một chuỗi ký tự. Chuỗi trong Go là bất biến (immutable).
  • Ví dụ:
    package main
    
    import "fmt"
    
    func main() {
    	var myString = "Chào Go!"
    	fmt.Println("Chuỗi của tôi:", myString) // Output: Chuỗi của tôi: Chào Go!
    }
  • Cách biểu diễn chuỗi:
    1. Dấu nháy kép (" "): Dùng cho các chuỗi thông thường, hỗ trợ các ký tự escape (\n, \t, v.v.).
    2. Dấu nháy ngược (` `): Dùng cho chuỗi thô (raw string literals). Mọi ký tự bên trong sẽ được giữ nguyên, bao gồm các ký tự ngắt dòng và không xử lý ký tự escape. Thích hợp cho biểu thức chính quy hoặc HTML/JSON.

Sử dụng gói stringsstrconv

Gói strings cung cấp nhiều hàm tiện ích để thao tác với chuỗi, còn strconv dùng để chuyển đổi giữa chuỗi và các kiểu dữ liệu khác.

package main

import (
	"fmt"
	"strconv"
	"strings"
)

func main() {
	// --- Gói strings ---
	sampleURL := "example.com/path"
	if !strings.HasPrefix(sampleURL, "http://") {
		sampleURL = fmt.Sprintf("http://%s", sampleURL)
	}
	fmt.Println("URL hoàn chỉnh:", sampleURL) // Output: URL hoàn chỉnh: http://example.com/path

	filePath := "/home/user/document.txt"
	fmt.Println("Có kết thúc bằng .txt không?", strings.HasSuffix(filePath, ".txt")) // Output: Có kết thúc bằng .txt không? true

	text := "Xin chào Go, Go thật tuyệt!"
	fmt.Println("Vị trí đầu tiên của 'Go':", strings.Index(text, "Go"))     // Output: Vị trí đầu tiên của 'Go': 9
	fmt.Println("Vị trí cuối cùng của 'Go':", strings.LastIndex(text, "Go")) // Output: Vị trí cuối cùng của 'Go': 13

	replacedText := strings.Replace(text, "Go", "Golang", 1) // Thay thế "Go" bằng "Golang" lần đầu tiên
	fmt.Println("Sau khi thay thế:", replacedText)            // Output: Sau khi thay thế: Xin chào Golang, Go thật tuyệt!

	fmt.Println("Số lần xuất hiện của 'Go':", strings.Count(text, "Go")) // Output: Số lần xuất hiện của 'Go': 2

	repeatedText := strings.Repeat("ABC", 3)
	fmt.Println("Lặp lại chuỗi:", repeatedText) // Output: Lặp lại chuỗi: ABCABCABC

	fmt.Println("Chữ hoa:", strings.ToUpper(text)) // Output: Chữ hoa: XIN CHÀO GO, GO THẬT TUYỆT!
	fmt.Println("Chữ thường:", strings.ToLower(text)) // Output: Chữ thường: xin chào go, go thật tuyệt!

	// Cắt khoảng trắng
	trimmedSpace := strings.TrimSpace("   một chuỗi có khoảng trắng   ")
	fmt.Println("Cắt khoảng trắng:", trimmedSpace) // Output: Cắt khoảng trắng: một chuỗi có khoảng trắng

	// Cắt ký tự cụ thể ở đầu và cuối
	trimmedChars := strings.Trim("---hello---", "-")
	fmt.Println("Cắt ký tự:", trimmedChars) // Output: Cắt ký tự: hello

	// Tách chuỗi thành slice
	words := strings.Fields("Apple Banana Cherry") // Tách bằng khoảng trắng
	fmt.Println("Các từ:", words)                 // Output: Các từ: [Apple Banana Cherry]

	parts := strings.Split("red,green,blue", ",")
	fmt.Println("Các phần:", parts)               // Output: Các phần: [red green blue]

	// Nối slice thành chuỗi
	joinedString := strings.Join([]string{"file1.txt", "file2.log"}, "-")
	fmt.Println("Nối chuỗi:", joinedString) // Output: Nối chuỗi: file1.txt-file2.log

	// --- Gói strconv ---
	numStr := strconv.Itoa(12345) // Chuyển số nguyên sang chuỗi
	fmt.Println("Số nguyên thành chuỗi:", numStr) // Output: Số nguyên thành chuỗi: 12345

	strNum := "987"
	parsedNum, err := strconv.Atoi(strNum) // Chuyển chuỗi sang số nguyên
	if err != nil {
		fmt.Println("Lỗi chuyển đổi:", err)
	} else {
		fmt.Println("Chuỗi thành số nguyên:", parsedNum) // Output: Chuỗi thành số nguyên: 987
	}
}

Kiểu Thời gian và Ngày tháng

Gói time cung cấp các chức năng để làm việc với thời gian.

  • Sử dụng kiểu time.Time để biểu diễn một thời điểm cụ thể.
  • time.Now() để lấy thời gian hiện tại.
  • Truy cập các thành phần thời gian như Day(), Minute(), Month(), Year().
  • time.Duration dùng để biểu diễn một khoảng thời gian (ví dụ: micro giây, mili giây, giây).
package main

import (
	"fmt"
	"time"
)

func main() {
	currentTime := time.Now()
	fmt.Println("Thời gian hiện tại:", currentTime) // Output: Thời gian hiện tại: 2023-10-27 10:30:00.123456789 +0700 ICT m=+0.000000001

	// Truy cập các thành phần
	fmt.Printf("Năm: %d, Tháng: %s, Ngày: %d\n", currentTime.Year(), currentTime.Month(), currentTime.Day())

	// Định dạng thời gian
	// Chuỗi định dạng "2006-01-02 15:04:05" là một chuỗi ma thuật trong Go, đại diện cho một thời điểm cụ thể (Jan 2, 2006, 3:04:05 PM)
	fmt.Println("Định dạng YYYY/MM/DD HH:MM:SS:", currentTime.Format("2006/01/02 15:04:05"))
	fmt.Println("Định dạng DD-MM-YYYY:", currentTime.Format("02-01-2006"))

	// Đo lường thời gian trôi qua
	startTime := time.Now().UnixNano()
	time.Sleep(150 * time.Millisecond) // Dừng chương trình 150 mili giây
	endTime := time.Now().UnixNano()

	elapsedMicroseconds := (endTime - startTime) / 1000
	fmt.Printf("Thời gian thực thi: %d micro giây\n", elapsedMicroseconds)
}

Kiểu Con trỏ (Pointers)

  • Trong Go, các biến kiểu thông thường lưu trữ trực tiếp giá trị (kiểu giá trị).
  • Để lấy địa chỉ bộ nhớ của một biến, sử dụng toán tử & (address-of). Ví dụ: &myVar.
  • Kiểu con trỏ là biến lưu trữ một địa chỉ bộ nhớ.
  • Để truy cập giá trị mà con trỏ đang trỏ tới (giải tham chiếu), sử dụng toán tử * (dereference). Ví dụ: *ptrVar.
package main

import "fmt"

func main() {
	var initialNum int = 50
	fmt.Println("Giá trị của initialNum:", initialNum) // Output: Giá trị của initialNum: 50
	fmt.Println("Địa chỉ của initialNum:", &initialNum) // Output: Địa chỉ của initialNum: 0xc0000140a0 (ví dụ)

	// Khai báo một con trỏ kiểu *int
	var ptrToNum *int

	// Gán địa chỉ của initialNum cho ptrToNum
	ptrToNum = &initialNum
	fmt.Println("Giá trị của con trỏ ptrToNum (địa chỉ):", ptrToNum) // Output: Giá trị của con trỏ ptrToNum (địa chỉ): 0xc0000140a0

	// Truy cập giá trị mà con trỏ đang trỏ tới
	fmt.Println("Giá trị mà ptrToNum trỏ tới:", *ptrToNum) // Output: Giá trị mà ptrToNum trỏ tới: 50

	// Thay đổi giá trị thông qua con trỏ
	*ptrToNum = 150
	fmt.Println("Giá trị mới của initialNum:", initialNum) // Output: Giá trị mới của initialNum: 150

	var anotherNum int = 99
	ptrToNum = &anotherNum // Bây giờ ptrToNum trỏ đến anotherNum
	*ptrToNum = 500       // Thay đổi giá trị của anotherNum
	fmt.Println("Giá trị của initialNum (không đổi):", initialNum) // Output: Giá trị của initialNum (không đổi): 150
	fmt.Println("Giá trị mới của anotherNum:", anotherNum)         // Output: Giá trị mới của anotherNum: 500
}

Điều khiển Luồng (Flow Control)

1. Câu lệnh if / else

package main

import "fmt"

func main() {
	isRaining := true
	if isRaining {
		fmt.Println("Trời đang mưa, hãy mang theo ô!")
	} else {
		fmt.Println("Trời trong xanh, không cần ô.")
	}

	score := 85
	if score >= 90 {
		fmt.Println("Điểm A")
	} else if score >= 70 {
		fmt.Println("Điểm B")
	} else {
		fmt.Println("Điểm C")
	}
}

2. Câu lệnh switch case

Câu lệnh switch trong Go rất linh hoạt, không cần break tường minh và có thể có nhiều điều kiện trong một case. Có thể sử dụng fallthrough để chuyển sang case tiếp theo.

package main

import "fmt"

func main() {
	dayOfWeek := 3
	switch dayOfWeek {
	case 1:
		fmt.Println("Chủ Nhật")
	case 2, 3, 4, 5, 6: // Có thể có nhiều giá trị trong một case
		fmt.Println("Ngày trong tuần")
	default:
		fmt.Println("Không phải ngày hợp lệ")
	}

	// switch với fallthrough
	grade := "B"
	switch grade {
	case "A":
		fmt.Println("Xuất sắc!")
		fallthrough // Chuyển sang case tiếp theo
	case "B":
		fmt.Println("Khá giỏi!")
		// Không có fallthrough, nên sẽ thoát khỏi switch
	case "C":
		fmt.Println("Trung bình")
	}

	// switch không có biểu thức, sử dụng điều kiện boolean
	hour := 14
	switch {
	case hour < 12:
		fmt.Println("Chào buổi sáng!")
	case hour >= 12 && hour < 18:
		fmt.Println("Chào buổi chiều!")
	default:
		fmt.Println("Chào buổi tối!")
	}

	// switch với short statement (khai báo biến trong switch)
	switch status := "active"; status {
	case "active":
		fmt.Println("Trạng thái hoạt động")
	case "inactive":
		fmt.Println("Trạng thái không hoạt động")
	default:
		fmt.Println("Trạng thái không xác định")
	}
}

Bài tập 11: Trò chơi Đoán số

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	rand.Seed(time.Now().UnixNano()) // Khởi tạo seed cho trình tạo số ngẫu nhiên
	secretNum := rand.Intn(100)      // Tạo số ngẫu nhiên từ 0 đến 99

	fmt.Println("Chào mừng đến với trò chơi Đoán Số!")
	fmt.Println("Tôi đã nghĩ một số từ 0 đến 99. Bạn hãy đoán xem!")

	for {
		var guess int
		fmt.Print("Nhập số bạn đoán: ")
		_, err := fmt.Scanf("%d\n", &guess) // Đọc số nguyên từ người dùng
		if err != nil {
			fmt.Println("Đầu vào không hợp lệ. Vui lòng nhập một số.")
			continue
		}

		switch {
		case guess == secretNum:
			fmt.Println("Chúc mừng! Bạn đã đoán đúng!")
			return // Thoát khỏi trò chơi
		case guess < secretNum:
			fmt.Println("Số bạn đoán nhỏ hơn số bí mật.")
		case guess > secretNum:
			fmt.Println("Số bạn đoán lớn hơn số bí mật.")
		}
	}
}

Bài tập 12: In hình tam giác

package main

import "fmt"

func printTrianglePattern() {
	var rows int
	fmt.Print("Nhập số hàng cho tam giác: ")
	_, err := fmt.Scanf("%d\n", &rows)
	if err != nil {
		fmt.Println("Đầu vào không hợp lệ.")
		return
	}

	for i := 1; i <= rows; i++ { // Duyệt qua từng hàng
		for j := 0; j < i; j++ { // Duyệt qua từng cột để in ký tự
			fmt.Print("*")
		}
		fmt.Println() // Xuống dòng sau mỗi hàng
	}
}

func main() {
	printTrianglePattern()
}

3. Câu lệnh for

Trong Go, for là câu lệnh vòng lặp duy nhất nhưng rất linh hoạt, có thể dùng thay thế cho whiledo-while.

package main

import "fmt"

func main() {
	// 1. Vòng lặp for truyền thống (với khởi tạo, điều kiện, thay đổi)
	for i := 0; i < 5; i++ {
		fmt.Printf("Vòng lặp truyền thống: %d\n", i)
	}

	// 2. Vòng lặp for như while (chỉ có điều kiện)
	count := 0
	for count < 3 {
		fmt.Printf("Vòng lặp như while: %d\n", count)
		count++
	}

	// 3. Vòng lặp vô hạn (infinite loop)
	// for {
	//    fmt.Println("Vòng lặp vô hạn (Ctrl+C để thoát)")
	//    time.Sleep(time.Second) // Để không làm tràn console
	// }

	// 4. Vòng lặp for-range (để duyệt qua slice, array, map, string, channel)
	greeting := "Chào Go Thế giới!"
	for index, char := range greeting {
		fmt.Printf("Ký tự tại index %d: %c (Unicode: %d)\n", index, char, char)
	}

	// 5. break và continue
	for i := 0; i < 10; i++ {
		if i%2 != 0 { // Nếu i là số lẻ
			continue // Bỏ qua lần lặp hiện tại, chuyển sang lần lặp tiếp theo
		}
		if i > 6 {
			break // Thoát khỏi vòng lặp khi i lớn hơn 6
		}
		fmt.Printf("Số chẵn nhỏ hơn 6: %d\n", i) // Output: 0, 2, 4, 6
	}
}

4. Câu lệnh gotolabel

goto được dùng để nhảy đến một nhãn (label) trong chương trình. Nên hạn chế sử dụng goto vì nó có thể làm mã khó đọc và bảo trì.

package main

import "fmt"

func main() {
	i := 0
StartLoop: // Định nghĩa một nhãn
	fmt.Println("Giá trị của i:", i)
	i++
	if i < 5 {
		goto StartLoop // Nhảy đến nhãn StartLoop
	}

	// Ví dụ với nested loops và nhãn
OuterLoop:
	for x := 0; x < 3; x++ {
		for y := 0; y < 3; y++ {
			if x == 1 && y == 1 {
				continue OuterLoop // Nhảy đến vòng lặp ngoài
			}
			fmt.Printf("x: %d, y: %d\n", x, y)
		}
	}
}

Hàm (Functions)

Đặc điểm của hàm trong Go:

  • Không hỗ trợ overloading: Trong cùng một gói, không thể có hai hàm cùng tên.
  • Hàm là "first-class citizen": Hàm có thể được gán cho biến, truyền làm tham số cho hàm khác, hoặc trả về từ một hàm khác.
  • Hỗ trợ hàm ẩn danh (anonymous functions)đa giá trị trả về.

Gán hàm cho biến

package main

import "fmt"

func add(a, b int) int {
	return a + b
}

func main() {
	// Gán hàm 'add' cho biến 'sumFunc'
	sumFunc := add
	fmt.Printf("Kiểu của sumFunc: %T\n", sumFunc) // Output: Kiểu của sumFunc: func(int, int) int

	result := sumFunc(20, 30) // Gọi hàm thông qua biến
	fmt.Println("Kết quả phép cộng:", result) // Output: Kết quả phép cộng: 50

	if sumFunc == add { // Có thể so sánh hàm
		fmt.Println("sumFunc và add trỏ đến cùng một hàm")
	}
}

Hàm bậc cao (Higher-Order Functions) và kiểu hàm

package main

import "fmt"

// Định nghĩa một kiểu hàm 'BinaryOperator'
type BinaryOperator func(int, int) int

// Hàm 'executeOperation' nhận một hàm BinaryOperator làm tham số
func executeOperation(op BinaryOperator, val1, val2 int) int {
	return op(val1, val2)
}

func multiply(a, b int) int {
	return a * b
}

func main() {
	// Gán hàm 'multiply' cho biến kiểu BinaryOperator
	mulOp := multiply

	// Truyền biến hàm vào 'executeOperation'
	result := executeOperation(mulOp, 10, 5)
	fmt.Println("Kết quả phép nhân:", result) // Output: Kết quả phép nhân: 50

	// Sử dụng một hàm ẩn danh trực tiếp
	divisionResult := executeOperation(func(a, b int) int {
		if b == 0 {
			return 0 // Tránh chia cho 0
		}
		return a / b
	}, 10, 2)
	fmt.Println("Kết quả phép chia:", divisionResult) // Output: Kết quả phép chia: 5
}

Cách truyền tham số vào hàm

  • Go sử dụng phương pháp truyền theo giá trị (pass by value) cho tất cả các loại tham số. Điều này có nghĩa là một bản sao của giá trị được truyền vào hàm.
  • Tuy nhiên, đối với các kiểu tham chiếu như slice, map, channel, interface và bản thân pointer, giá trị được sao chép là địa chỉ bộ nhớ mà chúng trỏ tới. Do đó, hàm vẫn có thể sửa đổi dữ liệu mà các tham chiếu này trỏ tới.
  • Ví dụ:
    package main
    
    import "fmt"
    
    // Hàm nhận một số nguyên (kiểu giá trị)
    func modifyInt(num int) {
    	num = 999 // Thay đổi 'num' trong hàm không ảnh hưởng đến biến gốc
    	fmt.Println("Trong modifyInt, num =", num)
    }
    
    // Hàm nhận một con trỏ số nguyên
    func modifyIntByPointer(ptr *int) {
    	*ptr = 999 // Thay đổi giá trị mà con trỏ trỏ tới, ảnh hưởng đến biến gốc
    	fmt.Println("Trong modifyIntByPointer, *ptr =", *ptr)
    }
    
    // Hàm nhận một slice (kiểu tham chiếu, header được sao chép)
    func modifySlice(s []int) {
    	s[0] = 100 // Thay đổi phần tử của slice, ảnh hưởng đến slice gốc
    	fmt.Println("Trong modifySlice, s =", s)
    }
    
    func main() {
    	a := 10
    	fmt.Println("Trước modifyInt, a =", a) // Output: Trước modifyInt, a = 10
    	modifyInt(a)
    	fmt.Println("Sau modifyInt, a =", a)   // Output: Sau modifyInt, a = 10 (không đổi)
    
    	b := 20
    	fmt.Println("Trước modifyIntByPointer, b =", b) // Output: Trước modifyIntByPointer, b = 20
    	modifyIntByPointer(&b)
    	fmt.Println("Sau modifyIntByPointer, b =", b)   // Output: Sau modifyIntByPointer, b = 999 (đã đổi)
    
    	mySlice := []int{1, 2, 3}
    	fmt.Println("Trước modifySlice, mySlice =", mySlice) // Output: Trước modifySlice, mySlice = [1 2 3]
    	modifySlice(mySlice)
    	fmt.Println("Sau modifySlice, mySlice =", mySlice)   // Output: Sau modifySlice, mySlice = [100 2 3] (đã đổi)
    }

Giá trị trả về được đặt tên

Bạn có thể đặt tên cho các giá trị trả về của hàm. Khi đó, bạn có thể sử dụng trực tiếp các tên này trong hàm và chỉ cần gọi return (naked return).

func processNumbers(a, b int) (sum int, product int) {
	sum = a + b
	product = a * b
	return // Tương đương với return sum, product
}

Bỏ qua giá trị trả về bằng _

Sử dụng _ để bỏ qua các giá trị trả về mà bạn không cần.

sum, _ := processNumbers(10, 5) // Chỉ lấy tổng, bỏ qua tích
fmt.Println("Tổng:", sum)

Tham số biến đổi (Variadic Parameters)

Hàm có thể nhận số lượng tham số tùy ý của cùng một kiểu bằng cách sử dụng ....

// Hàm nhận 0 hoặc nhiều số nguyên
func sumAll(numbers ...int) int {
	total := 0
	for _, num := range numbers {
		total += num
	}
	return total
}

// Hàm nhận ít nhất một số nguyên và các số nguyên khác tùy ý
func sumWithBase(base int, others ...int) int {
	total := base
	for _, num := range others {
		total += num
	}
	return total
}

Tham số biến đổi (ví dụ: numbers) sẽ được xử lý bên trong hàm như một slice.

Mục đích của defer

Câu lệnh defer được dùng để hoãn việc thực thi một hàm cho đến khi hàm bao quanh nó kết thúc. Điều này thường được dùng cho việc dọn dẹp tài nguyên.

  • Hàm defer được thực thi ngay trước khi hàm chứa nó trả về.
  • Nếu có nhiều câu lệnh defer, chúng sẽ được thực thi theo thứ tự LIFO (Last-In, First-Out).
  • Các tham số của hàm defer được đánh giá tại thời điểm defer được khai báo, không phải khi nó thực sự chạy.
package main

import "fmt"

func exampleDefer() {
	i := 0
	defer fmt.Println("Giá trị của i khi defer chạy:", i) // i được đánh giá là 0 tại đây
	i++
	fmt.Println("Giá trị của i sau tăng:", i) // Output: Giá trị của i sau tăng: 1
	return
}

func multipleDefer() {
	for i := 0; i < 3; i++ {
		defer fmt.Printf("%d ", i) // Output: 2 1 0 (LIFO)
	}
	fmt.Println("Hoàn thành vòng lặp.")
}

func main() {
	exampleDefer()
	fmt.Println("---")
	multipleDefer()
	fmt.Println("\n---")

	// Ví dụ thực tế:
	// 1. Đóng file
	// file, err := os.Open("some.txt")
	// if err != nil { /* xử lý lỗi */ }
	// defer file.Close() // Đảm bảo file sẽ được đóng

	// 2. Giải phóng khóa (lock)
	// var mu sync.Mutex
	// mu.Lock()
	// defer mu.Unlock() // Đảm bảo khóa sẽ được giải phóng

	// 3. Đóng kết nối cơ sở dữ liệu
	// dbConn := openDatabase()
	// defer dbConn.Close()
}

Tính nguyên tử (Atomicity)

Tính nguyên tử đề cập đến đặc tính mà một hoặc nhiều thao tác được thực hiện bởi CPU sẽ không bị gián đoạn. Các thao tác này được xem như một đơn vị không thể chia cắt; chúng hoặc là thực hiện tất cả, hoặc không thực hiện gì cả, và không có trạng thái trung gian nào được nhìn thấy từ bên ngoài.

Trong Go, gói sync/atomic cung cấp các hàm cho các phép toán nguyên tử trên các biến số học. Các phép toán này đảm bảo an toàn khi truy cập đồng thời mà không cần khóa mutex. Điều quan trọng là các tham số của các hàm nguyên tử phải là kiểu con trỏ, cho phép Go sử dụng các chỉ thị CPU đặc biệt để đảm bảo tính nguyên tử.

atomic.Value để đảm bảo an toàn đọc/ghi cho mọi giá trị

Gói atomic cũng cung cấp atomic.Value để lưu trữ và tải bất kỳ giá trị nào một cách nguyên tử. Điều này hữu ích để cập nhật cấu hình hoặc các đối tượng lớn mà không cần khóa mutex cho mỗi lần đọc/ghi.

Định nghĩa Struct

struct là một kiểu dữ liệu do người dùng định nghĩa, cho phép nhóm các trường (field) với các kiểu dữ liệu khác nhau lại với nhau thành một đơn vị logic.

1. Khai báo Struct

type Identifier struct {
    field1 type1
    field2 type2
    // ...
}

// Ví dụ:
type User struct {
    Name    string
    Age     int
    Email   string
    IsAdmin bool
}

2. Truy cập trường trong Struct

Sử dụng toán tử dấu chấm (.) để truy cập các trường.

var newUser User
newUser.Name = "Alice"
newUser.Age = 30
newUser.Email = "alice@example.com"
newUser.IsAdmin = false

fmt.Printf("Tên: %s, Tuổi: %d\n", newUser.Name, newUser.Age)

3. Ba cách khởi tạo Struct

package main

import "fmt"

type Product struct {
	ID    int
	Name  string
	Price float64
}

func main() {
	// 1. Khai báo biến struct và gán giá trị sau đó
	var p1 Product
	p1.ID = 1
	p1.Name = "Laptop"
	p1.Price = 1200.00
	fmt.Println("Product 1:", p1)

	// 2. Sử dụng new() để tạo con trỏ đến struct (khởi tạo với zero-value)
	p2 := new(Product) // p2 là *Product
	p2.ID = 2
	p2.Name = "Mouse"
	p2.Price = 25.50
	fmt.Println("Product 2:", *p2) // Truy cập giá trị thông qua con trỏ

	// 3. Sử dụng địa chỉ của struct literal (&Struct{})
	p3 := &Product{ // p3 là *Product
		ID:    3,
		Name:  "Keyboard",
		Price: 75.00,
	}
	fmt.Println("Product 3:", *p3)

	// Cũng có thể khởi tạo trực tiếp struct literal (giá trị struct)
	p4 := Product{
		ID:    4,
		Name:  "Monitor",
		Price: 300.00,
	}
	fmt.Println("Product 4:", p4)
}

Với các biến con trỏ (ví dụ p2, p3), bạn có thể truy cập trường trực tiếp bằng p2.ID hoặc (*p2).ID. Go tự động giải tham chiếu cho bạn.

4. Bố cục bộ nhớ của Struct

Các trường của một struct được lưu trữ liên tiếp trong bộ nhớ. Điều này giúp truy cập hiệu quả.

5. Định nghĩa Cấu trúc dữ liệu liên kết (ví dụ: Linked List)

type Node struct {
    Value int
    Next  *Node // Con trỏ tới Node tiếp theo
}

// Ví dụ Doubly Linked List
type DoublyNode struct {
    Value int
    Next  *DoublyNode
    Prev  *DoublyNode // Con trỏ tới Node trước đó
}

// Ví dụ Binary Tree
type TreeNode struct {
    Value int
    Left  *TreeNode  // Con trỏ tới nút con bên trái
    Right *TreeNode // Con trỏ tới nút con bên phải
}

6. Struct và chuyển đổi kiểu

Một struct là một kiểu dữ liệu riêng biệt và không thể ép kiểu sang một struct khác, ngay cả khi chúng có cùng các trường. Tuy nhiên, bạn có thể khai báo một kiểu mới dựa trên một struct hiện có.

type OldFormat struct {
    ID int
}

type NewFormat OldFormat // NewFormat là một kiểu mới, không thể gán trực tiếp cho OldFormat

var oldData OldFormat
var newData NewFormat

// oldData = newData // Lỗi: cannot use newData (type NewFormat) as type OldFormat in assignment
oldData = OldFormat(newData) // Cần chuyển đổi tường minh

7. Factory Pattern cho Struct

Go không có hàm tạo (constructor) theo nghĩa truyền thống. Bạn có thể sử dụng hàm factory để tạo và khởi tạo struct.

// package models
type employee struct { // `employee` bắt đầu bằng chữ thường, nên nó là riêng tư
    name string
    age  int
}

// NewEmployee là một hàm factory để tạo và trả về một con trỏ tới employee
// Hàm factory bắt đầu bằng chữ hoa để có thể truy cập từ bên ngoài gói
func NewEmployee(name string, age int) *employee {
    return &employee{ // Trả về địa chỉ của một struct employee được khởi tạo
        name: name,
        age:  age,
    }
}

// trong main package
// emp := models.NewEmployee("David", 35)

8. makenew

  • make: Dùng để khởi tạo các kiểu dữ liệu tham chiếu như map, slice, channel. Nó trả về một giá trị đã được khởi tạo.
  • new: Dùng để cấp phát bộ nhớ cho bất kỳ kiểu dữ liệu nào (kiểu giá trị hoặc kiểu tham chiếu) và trả về một con trỏ tới bộ nhớ đã được cấp phát, khởi tạo với zero-value của kiểu đó.

9. Struct Tags

Bạn có thể thêm các thẻ (tag) vào các trường của struct. Các thẻ này là các chuỗi metadata và thường được sử dụng bởi các thư viện reflection (ví dụ: để serialize/deserialize JSON).

type Student struct {
	Name  string `json:"student_name" db:"full_name"` // Thẻ JSON và DB
	Age   int    `json:"age"`
	Score int    `json:"-"` // Dấu gạch nối để bỏ qua trường này khi serialize/deserialize
}

10. Trường ẩn danh (Anonymous Fields)

Các trường trong struct có thể không có tên, khi đó tên của trường sẽ là tên kiểu của nó.

type Car struct {
    Manufacturer string
    Model        string
}

type Truck struct {
    Car            // Trường ẩn danh kiểu Car
    LoadCapacity int // Trường thông thường
    int            // Một trường ẩn danh kiểu int
}

func main() {
    myTruck := Truck{
        Car:            Car{Manufacturer: "Ford", Model: "F-150"},
        LoadCapacity: 2000,
        int:            100, // Gán giá trị cho trường ẩn danh int
    }
    fmt.Println(myTruck.Model)         // Có thể truy cập trực tiếp các trường của Car
    fmt.Println(myTruck.LoadCapacity)
    fmt.Println(myTruck.int)           // Truy cập trường int ẩn danh
    fmt.Println(myTruck.Car.Manufacturer) // Cách truy cập tường minh
}

11. Xử lý xung đột trường ẩn danh

Nếu có nhiều trường ẩn danh hoặc trường thông thường có cùng tên, bạn cần chỉ định rõ đường dẫn để truy cập.

type Engine struct {
    Power int
}

type Wheel struct {
    Size int
}

type Vehicle struct {
    Engine // Trường ẩn danh
    Wheel  // Trường ẩn danh
    Speed  int
    Power  int // Trường thông thường trùng tên với Engine.Power
}

func main() {
    v := Vehicle{}
    v.Engine.Power = 100 // Truy cập trường Power của Engine
    v.Wheel.Size = 18
    v.Speed = 200
    v.Power = 250        // Truy cập trường Power của Vehicle
    fmt.Println(v.Engine.Power) // Output: 100
    fmt.Println(v.Power)        // Output: 250
}

12. Phương thức (Methods)

Phương thức trong Go là các hàm được gắn với một kiểu cụ thể (receiver). Bất kỳ kiểu dữ liệu nào do người dùng định nghĩa (struct, int, string, v.v.) đều có thể có phương thức.

Cú pháp: func (receiver Type) MethodName(parameters) (returnValues) {}

type MyInt int

func (m MyInt) IsEven() bool { // Phương thức IsEven cho kiểu MyInt
	return m%2 == 0
}

type Point struct {
	X, Y int
}

func (p Point) GetCoordinates() (int, int) { // Phương thức GetCoordinates cho kiểu Point
	return p.X, p.Y
}

func main() {
	var num MyInt = 4
	fmt.Println("4 có phải số chẵn không?", num.IsEven()) // Output: 4 có phải số chẵn không? true

	origin := Point{X: 0, Y: 0}
	x, y := origin.GetCoordinates()
	fmt.Printf("Tọa độ điểm gốc: (%d, %d)\n", x, y) // Output: Tọa độ điểm gốc: (0, 0)
}

13. Khác biệt giữa Hàm và Phương thức

  • Hàm: Được gọi trực tiếp (ví dụ: FunctionName(args)).
  • Phương thức: Được gọi thông qua một biến của kiểu receiver (ví dụ: variable.MethodName(args)).

14. Receiver kiểu con trỏ vs. Receiver kiểu giá trị

Tương tự như cách truyền tham số vào hàm:

  • Receiver kiểu giá trị: Phương thức nhận một bản sao của giá trị receiver. Thay đổi bên trong phương thức không ảnh hưởng đến biến gốc.
  • Receiver kiểu con trỏ: Phương thức nhận một bản sao của con trỏ receiver (tức là địa chỉ của biến gốc). Thay đổi thông qua con trỏ sẽ ảnh hưởng đến biến gốc. Nên dùng khi cần sửa đổi receiver hoặc khi receiver là một struct lớn để tránh chi phí sao chép.

15. Kiểm soát truy cập Phương thức

Cũng như các hàm và trường, tên phương thức bắt đầu bằng chữ hoa là có thể xuất khẩu, chữ thường là riêng tư.

16. Kế thừa (Inheritance)

Go không có cơ chế kế thừa lớp theo nghĩa OOP truyền thống. Thay vào đó, nó sử dụng nhúng cấu trúc (struct embedding). Khi một struct nhúng một struct ẩn danh khác, nó sẽ tự động "kế thừa" các phương thức và trường của struct được nhúng.

type Person struct {
    Name string
}

func (p Person) Greet() {
    fmt.Printf("Chào, tôi là %s.\n", p.Name)
}

type Employee struct {
    Person // Nhúng struct Person ẩn danh
    Role   string
}

func main() {
    e := Employee{
        Person: Person{Name: "Bob"},
        Role:   "Developer",
    }
    e.Greet() // Employee có thể gọi phương thức Greet của Person
    fmt.Println("Vai trò:", e.Role)
}

17. Đa kế thừa (Multiple Inheritance)

Bạn có thể nhúng nhiều struct ẩn danh để đạt được hiệu ứng "đa kế thừa".

18. Thực hiện phương thức String()

Nếu một kiểu dữ liệu thực hiện phương thức String() string, thì hàm fmt.Println() (và các hàm định dạng khác) sẽ tự động gọi phương thức này để biểu diễn giá trị của biến.

type CustomType struct {
    ID    int
    Value string
}

func (ct CustomType) String() string {
    return fmt.Sprintf("ID: %d, Giá trị: %s", ct.ID, ct.Value)
}

func main() {
    data := CustomType{ID: 1, Value: "Thông tin mẫu"}
    fmt.Println(data) // Output: ID: 1, Giá trị: Thông tin mẫu
}

Giao diện (Interfaces)

interface định nghĩa một tập hợp các phương thức mà một kiểu dữ liệu phải thực hiện. Go interfaces không chứa bất kỳ trường dữ liệu nào, chỉ có chữ ký phương thức.

1. Định nghĩa Interface

type Shape interface {
    Area() float64       // Phương thức Area trả về float64
    Perimeter() float64 // Phương thức Perimeter trả về float64
}

2. Interface là một kiểu

Một biến kiểu interface có thể giữ bất kỳ giá trị nào của kiểu dữ liệu thực hiện tất cả các phương thức của interface đó.

var s Shape // Biến 's' có kiểu interface Shape
// s có thể chứa một Circle hoặc Rectangle nếu chúng implement Shape

3. Thực hiện Interface (Implicit Implementation)

  • Go không có từ khóa implements. Một kiểu dữ liệu tự động thực hiện một interface nếu nó cung cấp tất cả các phương thức mà interface đó yêu cầu.
  • Một kiểu có thể thực hiện nhiều interface.
  • Nếu một kiểu chỉ thực hiện một phần các phương thức của interface, nó không thực hiện interface đó.
type Rectangle struct {
    Width, Height float64
}

func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

func (r Rectangle) Perimeter() float64 {
    return 2 * (r.Width + r.Height)
}

type Circle struct {
    Radius float64
}

func (c Circle) Area() float64 {
    return 3.14159 * c.Radius * c.Radius
}

func (c Circle) Perimeter() float6    // Lỗi: float chứ không phải float64
func (c Circle) Perimeter() float64 { // Sửa lỗi
    return 2 * 3.14159 * c.Radius
}

func main() {
    var myShape Shape // Khai báo biến interface
    
    myShape = Rectangle{Width: 5, Height: 3}
    fmt.Printf("Hình chữ nhật - Diện tích: %.2f, Chu vi: %.2f\n", myShape.Area(), myShape.Perimeter())

    myShape = Circle{Radius: 4}
    fmt.Printf("Hình tròn - Diện tích: %.2f, Chu vi: %.2f\n", myShape.Area(), myShape.Perimeter())
}

4. Đa hình (Polymorphism)

Interface cho phép bạn xử lý các đối tượng thuộc các kiểu khác nhau một cách thống nhất, miễn là chúng thực hiện cùng một interface.

5. Lồng Interface (Nested Interfaces)

Một interface có thể nhúng các interface khác, tạo ra một interface mới chứa tất cả các phương thức của các interface được nhúng.

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err)
}

// ReadWriter kết hợp cả Reader và Writer
type ReadWriter interface {
    Reader
    Writer
}

6. Khẳng định kiểu (Type Assertion)

Để truy cập kiểu cơ bản của một giá trị được lưu trữ trong interface, bạn có thể sử dụng khẳng định kiểu. Có hai dạng:

  • value := interfaceVar.(ConcreteType): Sẽ panic nếu kiểu không khớp.
  • value, ok := interfaceVar.(ConcreteType): An toàn hơn, ok sẽ là true nếu kiểu khớp, ngược lại là false.
package main

import "fmt"

func main() {
	var i interface{} = "Hello Go" // interface{} là empty interface, có thể chứa mọi kiểu

	s := i.(string) // Khẳng định kiểu an toàn
	fmt.Println(s)  // Output: Hello Go

	s, ok := i.(string)
	fmt.Println(s, ok) // Output: Hello Go true

	f, ok := i.(float64)
	fmt.Println(f, ok) // Output: 0 false (không panic)

	// f2 := i.(float64) // Sẽ panic nếu kiểu không khớp
	// fmt.Println(f2)
}

7. type switch cho khẳng định kiểu

Sử dụng switch với .(type) để kiểm tra nhiều kiểu khác nhau một cách hiệu quả.

package main

import "fmt"

func classify(items ...interface{}) {
	for i, x := range items {
		switch v := x.(type) { // Khai báo biến 'v' chứa giá trị đã được chuyển đổi
		case bool:
			fmt.Printf("Tham số #%d là boolean: %t\n", i, v)
		case float64:
			fmt.Printf("Tham số #%d là float64: %.2f\n", i, v)
		case int, int64: // Có thể kiểm tra nhiều kiểu cùng lúc
			fmt.Printf("Tham số #%d là integer: %d\n", i, v)
		case nil:
			fmt.Printf("Tham số #%d là nil\n", i)
		case string:
			fmt.Printf("Tham số #%d là chuỗi: \"%s\"\n", i, v)
		default:
			fmt.Printf("Tham số #%d có kiểu không xác định: %T\n", i, v)
		}
	}
}

func main() {
	classify(10, "Go", true, 3.14, nil, []int{1, 2}, map[string]string{"a": "b"})
}

8. Empty Interface (interface{})

Interface rỗng (interface{}) không có bất kỳ phương thức nào. Vì mọi kiểu dữ liệu đều thực hiện 0 phương thức, nên mọi giá trị đều có thể được gán cho một biến kiểu interface{}.

var anything interface{}
anything = 100
fmt.Println(anything) // Output: 100
anything = "một chuỗi bất kỳ"
fmt.Println(anything) // Output: một chuỗi bất kỳ

9. Kiểm tra xem một biến có thực hiện một interface cụ thể không

type Stringer interface {
    String() string
}

type MyStruct struct {
    Name string
}

func (ms MyStruct) String() string {
    return "MyStruct: " + ms.Name
}

func main() {
    var myVal MyStruct = MyStruct{Name: "Test"}

    // Kiểm tra và khẳng định kiểu
    if sVal, ok := interface{}(myVal).(Stringer); ok {
        fmt.Printf("myVal thực hiện Stringer: %s\n", sVal.String())
    } else {
        fmt.Println("myVal không thực hiện Stringer")
    }

    // Một ví dụ khác, nếu không có String() method
    type AnotherStruct struct {}
    var anotherVal AnotherStruct
    if _, ok := interface{}(anotherVal).(Stringer); ok {
        fmt.Println("anotherVal thực hiện Stringer")
    } else {
        fmt.Println("anotherVal không thực hiện Stringer")
    }
}

Sự khác biệt giữa Kiểu con trỏ và Kiểu giá trị (Chi tiết)

Trong Go, mọi thứ đều được truyền theo giá trị. Tuy nhiên, điều quan trọng là hiểu giá trị nào đang được truyền:

  • Kiểu giá trị (int, float, bool, string, array, struct nhỏ): Khi gán hoặc truyền vào hàm, một bản sao hoàn chỉnh của dữ liệu được tạo. Thay đổi bản sao không ảnh hưởng đến bản gốc.
  • Kiểu tham chiếu (slice, map, channel, function, pointer): Giá trị thực sự của các kiểu này là một cấu trúc dữ liệu nhỏ (gọi là "header") chứa một con trỏ tới dữ liệu cơ bản trên heap. Khi gán hoặc truyền vào hàm, bản sao của *header* này được tạo. Con trỏ trong bản sao vẫn trỏ đến *cùng một dữ liệu cơ bản* với bản gốc. Do đó, thay đổi dữ liệu cơ bản thông qua bản sao sẽ ảnh hưởng đến bản gốc.

Thí nghiệm với kiểu cơ bản

package main

import "fmt"

func main() {
	fmt.Println("#### Thí nghiệm kiểu cơ bản ####")
	valA := "originalA"
	valB := "originalB"

	fmt.Printf("Trước gán: valA = %s (&valA = %p), valB = %s (&valB = %p)\n", valA, &valA, valB, &valB)

	valA = valB // Gán giá trị của valB cho valA. Tạo một bản sao mới của chuỗi "originalB" và gán cho valA.

	fmt.Printf("Sau gán:   valA = %s (&valA = %p), valB = %s (&valB = %p)\n", valA, &valA, valB, &valB)

	valA = "changedA" // Thay đổi valA không ảnh hưởng đến valB

	fmt.Printf("Sau thay đổi valA: valA = %s (&valA = %p), valB = %s (&valB = %p)\n", valA, &valA, valB, &valB)
	// Kết quả: valA và valB có giá trị độc lập và địa chỉ khác nhau.
}

Thí nghiệm với kiểu tham chiếu (Struct)

package main

import "fmt"

type UserProfile struct {
	Name string
	Age  int
}

func main() {
	fmt.Println("\n#### Thí nghiệm kiểu tham chiếu (Struct Pointer) ####")
	user1 := &UserProfile{Name: "Alice", Age: 30} // user1 là *UserProfile
	user2 := &UserProfile{Name: "Bob", Age: 25}   // user2 là *UserProfile

	fmt.Printf("Trước gán: user1 = %v (&user1 = %p), user2 = %v (&user2 = %p)\n", user1, &user1, user2, &user2)
	fmt.Printf("         user1.Name (&user1.Name = %p), user2.Name (&user2.Name = %p)\n", &user1.Name, &user2.Name)

	user1 = user2 // user1 bây giờ trỏ tới cùng địa chỉ mà user2 trỏ tới

	fmt.Printf("Sau gán:   user1 = %v (&user1 = %p), user2 = %v (&user2 = %p)\n", user1, &user1, user2, &user2)
	fmt.Printf("         user1.Name (&user1.Name = %p), user2.Name (&user2.Name = %p)\n", &user1.Name, &user2.Name)

	user1.Name = "Charlie" // Thay đổi qua user1 sẽ thay đổi dữ liệu mà user2 cũng đang trỏ tới

	fmt.Printf("Sau thay đổi user1.Name: user1 = %v, user2 = %v\n", user1, user2)
	// Kết quả: user1 và user2 trỏ đến cùng một đối tượng UserProfile. Thay đổi qua user1 ảnh hưởng đến user2.
}

Thí nghiệm với Slice (hiện tượng "lạ")

Một slice là một kiểu tham chiếu, nhưng bản thân nó là một header chứa con trỏ tới mảng bên dưới, độ dài và dung lượng. Khi bạn truyền một slice vào hàm, header này được sao chép theo giá trị. Bản sao của header vẫn trỏ tới cùng mảng bên dưới.

  • Nếu bạn sửa đổi các phần tử của slice thông qua bản sao của header, dữ liệu gốc sẽ bị thay đổi.
  • Nếu bạn gán nil cho bản sao của header slice trong hàm, nó chỉ làm thay đổi bản sao đó, không ảnh hưởng đến slice gốc (vì bạn đang thay đổi header, không phải dữ liệu cơ bản).
package main

import "fmt"

func modifySliceHeader(data []string) {
	fmt.Printf("  modifySliceHeader: Địa chỉ header slice (bản sao): %p\n", &data)
	data = nil // Chỉ gán nil cho bản sao của header slice trong hàm
	fmt.Println("  modifySliceHeader: Sau gán nil, data =", data)
}

func modifySliceElements(data []string) {
	fmt.Printf("  modifySliceElements: Địa chỉ header slice (bản sao): %p\n", &data)
	if len(data) > 1 {
		data[1] = "lisi" // Thay đổi phần tử của mảng cơ sở
	}
	fmt.Println("  modifySliceElements: Sau thay đổi phần tử, data =", data)
}

func main() {
	mySlice := []string{"zhang", "san"}
	fmt.Printf("main: Địa chỉ header slice gốc: %p\n", &mySlice)
	fmt.Println("main: Trước modifySliceHeader, mySlice =", mySlice) // Output: [zhang san]

	modifySliceHeader(mySlice) // Truyền bản sao của header slice
	fmt.Println("main: Sau modifySliceHeader, mySlice =", mySlice)   // Output: [zhang san] (không đổi)
	fmt.Println("--------------------------------")

	fmt.Println("main: Trước modifySliceElements, mySlice =", mySlice) // Output: [zhang san]
	modifySliceElements(mySlice) // Truyền bản sao của header slice
	fmt.Println("main: Sau modifySliceElements, mySlice =", mySlice)   // Output: [zhang lisi] (đã đổi)
}

Giải thích: Hàm modifySliceHeader nhận một bản sao của header slice mySlice. Khi data = nil, nó chỉ làm cho bản sao này trỏ đến nil, chứ không ảnh hưởng đến header slice gốc mySlice trong hàm main. Ngược lại, modifySliceElements thay đổi một phần tử của mảng cơ sở mà cả header gốc và bản sao đều trỏ tới, nên thay đổi được phản ánh.

Gói reflect

Gói reflect cho phép chương trình kiểm tra cấu trúc của các đối tượng (biến, kiểu) tại thời điểm chạy (runtime introspection). Điều này hữu ích cho việc xây dựng các thư viện serialization, ORM, v.v.

1. Các chức năng chính

  • reflect.TypeOf(i interface{}) Type: Lấy kiểu động của biến, trả về đối tượng reflect.Type.
  • reflect.ValueOf(i interface{}) Value: Lấy giá trị động của biến, trả về đối tượng reflect.Value.
  • Value.Kind() Kind: Trả về "loại" cơ bản của biến (ví dụ: Int, String, Struct, Ptr).
  • Value.Interface() interface{}: Chuyển đổi giá trị reflect.Value trở lại kiểu interface{}.

2. Các hằng số reflect.Kind()

reflect.Kind là một tập hợp các hằng số đại diện cho các loại kiểu dữ liệu cơ bản trong Go (ví dụ: Bool, Int, String, Slice, Struct, v.v.).

3. Ví dụ Reflection

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var myFloat float64 = 6.28
	fmt.Println("Kiểu tĩnh:", reflect.TypeOf(myFloat)) // Output: Kiểu tĩnh: float64

	v := reflect.ValueOf(myFloat)
	fmt.Println("Giá trị reflection:", v)     // Output: Giá trị reflection: 6.28
	fmt.Println("Kiểu reflection:", v.Type()) // Output: Kiểu reflection: float64
	fmt.Println("Loại reflection:", v.Kind()) // Output: Loại reflection: float64
	fmt.Println("Giá trị float:", v.Float())  // Output: Giá trị float: 6.28

	// Chuyển đổi ngược về interface{} và sau đó về kiểu cụ thể
	if val, ok := v.Interface().(float64); ok {
		fmt.Printf("Giá trị từ interface{}: %.2f\n", val) // Output: Giá trị từ interface{}: 6.28
	}
}

4. Lấy giá trị từ reflect.Value

Sử dụng các phương thức như Float(), Int(), String(), Bool() để lấy giá trị thực tế của biến.

5. Thay đổi giá trị biến bằng Reflection

Để thay đổi giá trị của một biến bằng reflection, bạn phải đảm bảo rằng giá trị reflect.Value bạn đang làm việc là "có địa chỉ" (addressable) và "có thể thiết lập" (settable). Điều này thường có nghĩa là bạn phải truyền một con trỏ đến biến.

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var num float64 = 1.0

	// Lỗi: v là bản sao của giá trị num, không có địa chỉ.
	// fv := reflect.ValueOf(num)
	// fv.SetFloat(3.14) // Sẽ gây panic

	// Cách đúng: Truyền con trỏ đến num
	ptrToNum := reflect.ValueOf(&num) // ptrToNum là reflect.Value của *float64
	fmt.Println("Có thể thiết lập?", ptrToNum.CanSet()) // Output: Có thể thiết lập? false (không thể thiết lập ptrToNum)

	// Lấy phần tử mà con trỏ trỏ tới (Elem)
	elemOfNum := ptrToNum.Elem() // elemOfNum là reflect.Value của float64 mà con trỏ trỏ tới
	fmt.Println("Có thể thiết lập?", elemOfNum.CanSet()) // Output: Có thể thiết lập? true

	elemOfNum.SetFloat(3.14)
	fmt.Printf("Giá trị mới của num: %.2f\n", num) // Output: Giá trị mới của num: 3.14
}

6. Thao tác với Struct bằng Reflection

  • reflect.Value.NumField(): Lấy số lượng trường trong struct.
  • reflect.Value.Field(i int) Value: Lấy giá trị của trường tại chỉ mục i.
  • reflect.Value.Method(n int) Value: Lấy giá trị của phương thức tại chỉ mục n.
  • Value.Call(args []Value) []Value: Gọi một phương thức đã lấy được.
package main

import (
	"fmt"
	"reflect"
)

type MyObject struct {
	Name    string
	Version int
}

func (mo MyObject) SayHello() string {
	return "Hello từ " + mo.Name
}

func main() {
	obj := MyObject{"GoApp", 1}
	objValue := reflect.ValueOf(&obj).Elem() // Lấy Value của struct, không phải con trỏ

	fmt.Printf("Struct có %d trường\n", objValue.NumField())

	// Duyệt qua các trường
	for i := 0; i < objValue.NumField(); i++ {
		field := objValue.Field(i)
		fieldType := objValue.Type().Field(i) // Lấy reflect.StructField để có tên
		fmt.Printf("Trường %d: %s (%v) = %v\n", i, fieldType.Name, field.Type(), field.Interface())
	}

	// Thay đổi giá trị trường (chỉ khi trường là exported và Value là settable)
	if objValue.Field(0).CanSet() { // Name là trường exported
		objValue.Field(0).SetString("GoApplication")
	}
	if objValue.Field(1).CanSet() { // Version là trường exported
		objValue.Field(1).SetInt(2)
	}
	fmt.Println("Struct sau thay đổi:", obj) // Output: Struct sau thay đổi: {GoApplication 2}

	// Gọi phương thức (phương thức phải là exported)
	method := reflect.ValueOf(obj).MethodByName("SayHello") // obj phải là giá trị, không phải con trỏ để gọi method
	if method.IsValid() {
		results := method.Call(nil) // nil vì không có tham số
		fmt.Println("Kết quả gọi SayHello:", results[0].Interface())
	}
}

Đọc và Ghi Terminal

1. Các biến file handle tiêu chuẩn

  • os.Stdin: Đầu vào chuẩn (bàn phím).
  • os.Stdout: Đầu ra chuẩn (màn hình).
  • os.Stderr: Đầu ra lỗi chuẩn (màn hình).

2. Đọc và ghi không đệm

Sử dụng gói fmt cho các thao tác I/O đơn giản.

package main

import (
	"fmt"
)

func main() {
	var name string
	var age int
	var height float32

	fmt.Print("Nhập tên, tuổi và chiều cao của bạn: ")
	// Đọc các giá trị cách nhau bởi khoảng trắng hoặc xuống dòng
	_, err := fmt.Scan(&name, &age, &height) // Trả về số lượng item được đọc và lỗi
	if err != nil {
		fmt.Println("Lỗi đọc đầu vào:", err)
		return
	}
	fmt.Printf("Bạn là %s, %d tuổi, cao %.2f mét.\n", name, age, height)

	// fmt.Scanln đọc cho đến khi gặp ký tự xuống dòng
	// fmt.Scanf đọc theo định dạng cụ thể

	// Đọc từ một chuỗi (giống terminal)
	inputString := "123 45.67 GoLang"
	var num int
	var fval float64
	var text string
	_, err = fmt.Sscanf(inputString, "%d %f %s", &num, &fval, &text)
	if err != nil {
		fmt.Println("Lỗi Sscanf:", err)
		return
	}
	fmt.Printf("Từ chuỗi: Số=%d, Float=%.2f, Chuỗi=%s\n", num, fval, text)
}

3. Đọc có bộ đệm (Buffered Input)

Gói bufio cung cấp các trình đọc/ghi có bộ đệm, hiệu quả hơn cho các thao tác I/O lớn.

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	reader := bufio.NewReader(os.Stdin)
	fmt.Print("Vui lòng nhập một dòng văn bản: ")
	input, err := reader.ReadString('\n') // Đọc cho đến khi gặp ký tự xuống dòng
	if err != nil {
		fmt.Println("Lỗi đọc đầu vào:", err)
		return
	}
	fmt.Printf("Đầu vào của bạn là: %s", input) // input đã bao gồm ký tự xuống dòng
}

Đọc và Ghi file

1. Đọc file

Sử dụng os.Open() để mở file, defer file.Close() để đảm bảo file được đóng.

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
)

func main() {
	filePath := "example.txt" // Đảm bảo file này tồn tại hoặc tạo nó
	// Tạo file mẫu nếu chưa có
	_ = os.WriteFile(filePath, []byte("Dòng một.\nDòng hai.\nDòng ba."), 0644)

	file, err := os.Open(filePath)
	if err != nil {
		fmt.Println("Lỗi mở file:", err)
		return
	}
	defer file.Close() // Đảm bảo file được đóng khi hàm kết thúc

	reader := bufio.NewReader(file)
	line, err := reader.ReadString('\n') // Đọc một dòng đầu tiên
	if err != nil && err != io.EOF {
		fmt.Println("Lỗi đọc dòng đầu tiên:", err)
		return
	}
	fmt.Printf("Đọc dòng đầu tiên: %s", line)

	// Đọc toàn bộ file từng dòng một
	fmt.Println("\n--- Đọc toàn bộ file ---")
	file.Seek(0, io.SeekStart) // Đưa con trỏ về đầu file để đọc lại
	for {
		line, err := reader.ReadString('\n')
		if err == io.EOF {
			break // Đã đến cuối file
		}
		if err != nil {
			fmt.Println("Lỗi đọc file:", err)
			return
		}
		fmt.Printf("Dòng: %s", line)
	}
}

2. Đếm ký tự trong file

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"unicode" // Sử dụng gói unicode để xử lý ký tự đa ngôn ngữ
)

type FileCharCount struct {
	Letters int
	Digits  int
	Spaces  int
	Others  int
}

func main() {
	filePath := "sample.txt"
	// Tạo file mẫu
	_ = os.WriteFile(filePath, []byte("Hello Go 123. Xin chào Việt Nam!\n"), 0644)

	file, err := os.Open(filePath)
	if err != nil {
		fmt.Println("Lỗi mở file:", err)
		return
	}
	defer file.Close()

	var charCounts FileCharCount
	reader := bufio.NewReader(file)

	for {
		r, _, err := reader.ReadRune() // Đọc từng rune (ký tự Unicode)
		if err == io.EOF {
			break
		}
		if err != nil {
			fmt.Println("Lỗi đọc ký tự:", err)
			return
		}

		switch {
		case unicode.IsLetter(r):
			charCounts.Letters++
		case unicode.IsDigit(r):
			charCounts.Digits++
		case unicode.IsSpace(r):
			charCounts.Spaces++
		default:
			charCounts.Others++
		}
	}

	fmt.Printf("Thống kê ký tự trong file:\n")
	fmt.Printf("  Chữ cái: %d\n", charCounts.Letters)
	fmt.Printf("  Chữ số: %d\n", charCounts.Digits)
	fmt.Printf("  Khoảng trắng: %d\n", charCounts.Spaces)
	fmt.Printf("  Khác: %d\n", charCounts.Others)
}

3. Ghi file

Sử dụng os.OpenFile() để mở file với các cờ (flags) và quyền (permissions).

  • Cờ mở file: os.O_WRONLY (chỉ ghi), os.O_CREATE (tạo file nếu không tồn tại), os.O_RDONLY (chỉ đọc), os.O_RDWR (đọc và ghi), os.O_TRUNC (cắt ngắn file về 0).
  • Quyền file: Dùng hệ bát phân (ví dụ: 0666 cho đọc/ghi bởi mọi người, 0644 cho chủ sở hữu đọc/ghi, người khác chỉ đọc).
package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	outputFilePath := "output.txt"
	// Mở file để ghi (O_WRONLY), tạo file nếu không tồn tại (O_CREATE), và cắt ngắn file nếu đã có (O_TRUNC)
	file, err := os.OpenFile(outputFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		fmt.Printf("Lỗi khi tạo/mở file: %v\n", err)
		return
	}
	defer file.Close()

	writer := bufio.NewWriter(file) // Sử dụng trình ghi có bộ đệm
	contentToWrite := "Đây là dòng văn bản sẽ được ghi vào file.\n"
	
	for i := 0; i < 5; i++ {
		_, err := writer.WriteString(fmt.Sprintf("Dòng %d: %s", i+1, contentToWrite))
		if err != nil {
			fmt.Printf("Lỗi ghi file: %v\n", err)
			return
		}
	}

	err = writer.Flush() // Đảm bảo tất cả dữ liệu trong bộ đệm được ghi ra file
	if err != nil {
		fmt.Printf("Lỗi flush bộ đệm: %v\n", err)
		return
	}
	fmt.Println("Ghi file thành công vào:", outputFilePath)
}

4. Sao chép file

Gói io cung cấp hàm Copy tiện lợi để sao chép nội dung từ một Reader sang một Writer.

package main

import (
	"fmt"
	"io"
	"os"
)

// CopyFile sao chép nội dung từ srcName sang dstName
func CopyFile(dstName, srcName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		return
	}
	defer src.Close()

	dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
	if err != nil {
		return
	}
	defer dst.Close()

	return io.Copy(dst, src) // Sao chép dữ liệu từ src sang dst
}

func main() {
	// Tạo file nguồn
	_ = os.WriteFile("source.txt", []byte("Nội dung từ file nguồn."), 0644)

	bytesCopied, err := CopyFile("destination.txt", "source.txt")
	if err != nil {
		fmt.Printf("Lỗi sao chép file: %v\n", err)
		return
	}
	fmt.Printf("Sao chép thành công %d bytes từ source.txt sang destination.txt\n", bytesCopied)
}

Tham số dòng lệnh

  • os.Args: Một slice kiểu string chứa tất cả các đối số dòng lệnh, với os.Args[0] là tên chương trình.
  • Gói flag: Cung cấp cơ chế phân tích cú pháp các cờ (flags) dòng lệnh một cách có cấu trúc.
    package main
    
    import (
    	"flag"
    	"fmt"
    )
    
    func main() {
    	// Khai báo các cờ
    	var verbose bool
    	var name string
    	var count int
    
    	flag.BoolVar(&verbose, "v", false, "Hiển thị thông tin chi tiết")
    	flag.StringVar(&name, "name", "Người dùng", "Tên của người dùng")
    	flag.IntVar(&count, "c", 1, "Số lần lặp")
    
    	flag.Parse() // Phân tích cú pháp các cờ từ os.Args
    
    	fmt.Printf("Chế độ chi tiết: %t\n", verbose)
    	fmt.Printf("Tên: %s\n", name)
    	fmt.Printf("Số lần lặp: %d\n", count)
    
    	// Các đối số không phải cờ sẽ có trong flag.Args()
    	fmt.Println("Các đối số còn lại:", flag.Args())
    
    	// Ví dụ chạy: go run your_program.go -v -name="Alice" -c 3 arg1 arg2
    }

Đồng bộ hóa (Synchronization)

Khi nhiều goroutine cùng truy cập và sửa đổi một tài nguyên chung, cần có cơ chế đồng bộ hóa để tránh tình trạng race condition và đảm bảo tính đúng đắn của dữ liệu.

1. Các cơ chế đồng bộ hóa

  • Mutex (Khóa tương hỗ): Một cơ chế khóa cơ bản. Chỉ một goroutine tại một thời điểm có thể giữ khóa mutex. Nếu một goroutine cố gắng lấy khóa đang được giữ, nó sẽ bị chặn cho đến khi khóa được giải phóng.
  • Read-Write Mutex (Khóa đọc-ghi): Cho phép nhiều goroutine đọc tài nguyên cùng lúc (đọc chia sẻ), nhưng chỉ một goroutine được phép ghi tại một thời điểm (ghi độc quyền). Ghi ưu tiên hơn đọc.
  • Goroutine: Là các "luồng" nhẹ của Go, chúng tự nhiên có khả năng xử lý đồng thời.
  • runtime.Goexit(): Kết thúc goroutine hiện tại. Các hàm defer đã đăng ký sẽ được thực thi.
  • runtime.Gosched(): Tạm dừng goroutine hiện tại và cho phép các goroutine khác chạy. Nó sẽ được lên lịch lại để chạy sau.
  • runtime.GOMAXPROCS(): Kiểm soát số lượng lõi CPU mà chương trình Go được phép sử dụng.

Channel (Chi tiết)

Channel là một đường ống nơi các goroutine gửi và nhận giá trị.

1. Định nghĩa Channel

make(chan Kiểu_dữ_liệu, dung_lượng)

  • Kiểu_dữ_liệu: Kiểu dữ liệu mà channel sẽ truyền (ví dụ: int, string, struct{}).
  • dung_lượng (tùy chọn): Số lượng giá trị mà channel có thể lưu trữ trước khi người gửi bị chặn.
ch := make(chan int)          // Channel không có bộ đệm (unbuffered channel)
bufferedCh := make(chan string, 5) // Channel có bộ đệm dung lượng 5

2. Gửi và nhận dữ liệu

  • Gửi: ch <- value
  • Nhận: value := <-ch

3. Channel không bộ đệm (Unbuffered Channel)

  • Có dung lượng bằng 0 (len = 0). Không thể lưu trữ dữ liệu.
  • Hoạt động theo cơ chế đồng bộ: Người gửi sẽ bị chặn cho đến khi có người nhận sẵn sàng, và người nhận sẽ bị chặn cho đến khi có người gửi sẵn sàng.
  • Thường được dùng để đồng bộ hóa các goroutine.

4. Channel có bộ đệm (Buffered Channel)

  • Có dung lượng lớn hơn 0 (len > 0). Có thể lưu trữ dữ liệu lên đến dung lượng của nó.
  • Hoạt động theo cơ chế bất đồng bộ:
    • Người gửi sẽ bị chặn chỉ khi channel đầy.
    • Người nhận sẽ bị chặn chỉ khi channel trống.

5. Đóng Channel

  • Sử dụng close(ch) để đóng một channel. Không thể gửi thêm dữ liệu vào một channel đã đóng.
  • Người nhận có thể kiểm tra xem channel đã đóng hay chưa khi nhận dữ liệu: value, ok := <-ch. Nếu okfalse, channel đã đóng và value là zero-value của kiểu dữ liệu.
  • Đóng channel quá sớm (khi vẫn còn goroutine gửi) sẽ gây panic.
  • Có thể đọc từ một channel đã đóng. Nếu còn dữ liệu trong bộ đệm, nó sẽ đọc hết. Sau đó, nó sẽ đọc ra zero-value và ok sẽ là false.

6. Channel một chiều (Directional Channels)

Mặc định, channel là hai chiều. Bạn có thể chuyển đổi tường minh thành channel một chiều để tăng tính an toàn và rõ ràng của mã.

  • Channel chỉ gửi: var sendCh chan<- int
  • Channel chỉ nhận: var recvCh <-chan int

Bạn có thể gán một channel hai chiều cho một channel một chiều. Điều ngược lại không thể thực hiện.

7. Ví dụ Producer-Consumer với Channel

package main

import (
	"fmt"
	"time"
)

type Order struct {
	ID        int
	ProductID int
	Quantity  int
}

// Producer tạo ra các đơn hàng và gửi vào channel
func orderProducer(output chan<- Order, numOrders int) {
	for i := 1; i <= numOrders; i++ {
		newOrder := Order{
			ID:        i,
			ProductID: 100 + i,
			Quantity:  i * 10,
		}
		output <- newOrder // Gửi đơn hàng vào channel
		fmt.Printf("[Producer] Đã gửi đơn hàng #%d\n", newOrder.ID)
		time.Sleep(50 * time.Millisecond) // Giả lập độ trễ
	}
	close(output) // Đóng channel khi tất cả đơn hàng đã được gửi
	fmt.Println("[Producer] Hoàn thành việc tạo đơn hàng.")
}

// Consumer xử lý các đơn hàng nhận được từ channel
func orderConsumer(input <-chan Order, consumerID int) {
	for order := range input { // Vòng lặp for-range sẽ tự động nhận dữ liệu và kết thúc khi channel đóng
		fmt.Printf("[Consumer %d] Đã xử lý đơn hàng #%d (SP: %d, SL: %d)\n", consumerID, order.ID, order.ProductID, order.Quantity)
		time.Sleep(100 * time.Millisecond) // Giả lập độ trễ xử lý
	}
	fmt.Printf("[Consumer %d] Hoàn thành việc xử lý đơn hàng.\n", consumerID)
}

func main() {
	orderChannel := make(chan Order, 5) // Channel có bộ đệm dung lượng 5

	// Khởi chạy Producer trong một goroutine riêng
	go orderProducer(orderChannel, 10) // Tạo 10 đơn hàng

	// Khởi chạy Consumer trong một goroutine riêng
	go orderConsumer(orderChannel, 1)

	// Để chương trình không kết thúc ngay lập tức, đợi một khoảng thời gian
	time.Sleep(2 * time.Second)
	fmt.Println("Chương trình chính kết thúc.")
}

定时器 (Timers) và Bộ đếm thời gian (Tickers)

Gói time cung cấp các cấu trúc TimerTicker để thực hiện các hành động theo thời gian.

1. Timer

Một Timer là một sự kiện duy nhất sẽ kích hoạt sau một khoảng thời gian định trước.

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("Bắt đầu Timer...")
	// Tạo một timer sẽ kích hoạt sau 2 giây
	timer := time.NewTimer(2 * time.Second)

	// <-timer.C sẽ chặn cho đến khi timer kích hoạt
	<-timer.C
	fmt.Println("Timer đã kích hoạt sau 2 giây!")

	// Dừng Timer:
	// Có thể dừng timer trước khi nó kích hoạt. Nếu timer đã kích hoạt, Stop sẽ trả về false.
	stopped := timer.Stop()
	if stopped {
		fmt.Println("Timer đã được dừng.")
	} else {
		fmt.Println("Timer không thể dừng hoặc đã kích hoạt.")
	}

	// Đặt lại Timer:
	// Có thể đặt lại timer để nó kích hoạt lại với một khoảng thời gian mới.
	// Cần phải chắc chắn rằng không có giá trị nào đang chờ đọc trên channel C của timer
	// trước khi gọi Reset, hoặc đọc nó nếu có.
	timer.Reset(1 * time.Second)
	fmt.Println("Timer đã được đặt lại 1 giây.")
	<-timer.C
	fmt.Println("Timer đã kích hoạt lần nữa sau khi reset!")
}

2. Ticker (Bộ đếm thời gian định kỳ)

Một Ticker gửi các sự kiện định kỳ trên channel của nó sau mỗi khoảng thời gian định trước.

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("Bắt đầu Ticker...")
	// Tạo một ticker sẽ gửi tín hiệu mỗi 500 mili giây
	ticker := time.NewTicker(500 * time.Millisecond)

	done := make(chan bool)

	go func() {
		for {
			select {
			case t := <-ticker.C:
				fmt.Println("Ticker đã kích hoạt tại:", t.Format("15:04:05.000"))
			case <-done:
				fmt.Println("Goroutine ticker đã dừng.")
				return
			}
		}
	}()

	// Để ticker chạy trong 3 giây
	time.Sleep(3 * time.Second)
	ticker.Stop() // Dừng ticker để giải phóng tài nguyên
	done <- true // Báo hiệu goroutine ticker kết thúc

	fmt.Println("Ticker đã dừng trong main.")
	time.Sleep(100 * time.Millisecond) // Đợi goroutine kết thúc
}

Xử lý Timeout với select

select kết hợp với time.After() là một cách phổ biến để thêm thời gian chờ (timeout) vào các thao tác chặn.

package main

import (
	"fmt"
	"time"
)

func worker(done chan bool) {
	fmt.Println("Worker đang làm việc...")
	time.Sleep(3 * time.Second) // Giả lập công việc mất 3 giây
	done <- true
}

func main() {
	taskDone := make(chan bool, 1) // Sử dụng buffered channel để tránh deadlock nếu worker nhanh hơn timeout
	go worker(taskDone)

	select {
	case <-taskDone:
		fmt.Println("Công việc hoàn thành đúng hạn!")
	case <-time.After(2 * time.Second): // Thời gian chờ 2 giây
		fmt.Println("Timeout! Công việc không hoàn thành kịp.")
	}

	// Ví dụ khác: xử lý nhiều channel với timeout
	ch1 := make(chan string)
	ch2 := make(chan string)

	go func() {
		time.Sleep(1 * time.Second)
		ch1 <- "Dữ liệu từ ch1"
	}()

	go func() {
		time.Sleep(4 * time.Second)
		ch2 <- "Dữ liệu từ ch2"
	}()

	for i := 0; i < 2; i++ { // Đợi 2 lần nhận dữ liệu hoặc timeout
		select {
		case msg := <-ch1:
			fmt.Println("Nhận:", msg)
		case msg := <-ch2:
			fmt.Println("Nhận:", msg)
		case <-time.After(2 * time.Second):
			fmt.Println("Lặp", i+1, ": Đã hết thời gian chờ!")
		}
	}
}

Deadlock (Tắc nghẽn)

Deadlock là một tình huống trong đó hai hoặc nhiều tác vụ (goroutine) bị chặn vô thời hạn, mỗi tác vụ chờ đợi tài nguyên mà tác vụ khác đang giữ.

1. Các kịch bản Deadlock phổ biến

  • Deadlock trong một goroutine: Một goroutine tự chặn chính nó. Ví dụ: gửi vào một unbuffered channel mà không có goroutine nào khác nhận.
    // channel không bộ đệm sẽ gây deadlock nếu chỉ có một goroutine cố gắng gửi
    // func main() {
    //     ch := make(chan int)
    //     ch <- 1 // deadlock
    // }
  • Deadlock do thứ tự truy cập channel: Các goroutine chờ nhau theo một vòng tròn.
    // func main() {
    //     ch1 := make(chan int)
    //     ch2 := make(chan int)
    //     go func() {
    //         <-ch1 // Goroutine này chờ ch1
    //         ch2 <- 1
    //     }()
    //     <-ch2 // Main goroutine chờ ch2
    //     ch1 <- 1 // Main goroutine sẽ không bao giờ đến đây
    // }
  • Deadlock nhiều goroutine, nhiều channel/lock chéo nhau: Goroutine A giữ tài nguyên M và chờ N; Goroutine B giữ tài nguyên N và chờ M.
  • Tránh trộn lẫn Mutex/RWMutex với Channel: Điều này có thể dẫn đến các deadlock khó phát hiện và hiểu. Nên ưu tiên sử dụng channel để đồng bộ hóa các goroutine trong Go.

Khóa tương hỗ (Mutex)

sync.Mutex cung cấp một khóa tương hỗ đơn giản để bảo vệ một phần mã hoặc dữ liệu khỏi việc truy cập đồng thời.

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	counter int
	mu      sync.Mutex // Khai báo một Mutex
)

func incrementCounter() {
	mu.Lock()         // Khóa mutex trước khi truy cập biến counter
	defer mu.Unlock() // Đảm bảo mutex được mở khóa khi hàm kết thúc
	counter++
	fmt.Printf("Counter: %d\n", counter)
}

func main() {
	var wg sync.WaitGroup // Để đợi tất cả goroutine hoàn thành

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			incrementCounter()
		}()
	}

	wg.Wait() // Đợi tất cả goroutine hoàn thành
	fmt.Printf("Giá trị cuối cùng của Counter: %d\n", counter) // Output: Giá trị cuối cùng của Counter: 5
}

Khóa đọc-ghi (RWMutex)

sync.RWMutex cung cấp khả năng khóa cho cả đọc và ghi. Nhiều goroutine có thể đọc đồng thời (RLock/RUnlock), nhưng chỉ một goroutine được phép ghi (Lock/Unlock). Khóa ghi sẽ chặn mọi khóa đọc/ghi khác.

package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	sharedData int = 0
	rwMu       sync.RWMutex // Khai báo một RWMutex
)

func readData(id int) {
	rwMu.RLock() // Khóa đọc
	defer rwMu.RUnlock() // Mở khóa đọc
	fmt.Printf("Đọc bởi Goroutine %d: %d\n", id, sharedData)
	time.Sleep(100 * time.Millisecond) // Giả lập thời gian đọc
}

func writeData(id, value int) {
	rwMu.Lock() // Khóa ghi
	defer rwMu.Unlock() // Mở khóa ghi
	sharedData = value
	fmt.Printf("Ghi bởi Goroutine %d: Đã đặt giá trị thành %d\n", id, sharedData)
	time.Sleep(200 * time.Millisecond) // Giả lập thời gian ghi
}

func main() {
	var wg sync.WaitGroup

	// Khởi tạo các Goroutine đọc
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			readData(id)
		}(i)
	}

	// Khởi tạo một Goroutine ghi
	wg.Add(1)
	go func() {
		defer wg.Done()
		writeData(10, 50)
	}()

	// Khởi tạo thêm các Goroutine đọc sau khi ghi
	for i := 4; i <= 6; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			time.Sleep(50 * time.Millisecond) // Đảm bảo ghi có thời gian chạy trước
			readData(id)
		}(i)
	}

	wg.Wait()
	fmt.Println("Hoàn thành chương trình RWMutex.")
}

Biến điều kiện (Condition Variables)

sync.Cond được dùng để cho phép các goroutine đợi một điều kiện cụ thể trước khi tiếp tục thực thi. Nó luôn được liên kết với một sync.Locker (thường là sync.Mutex).

Quy trình sử dụng:

  1. Tạo một sync.Cond, liên kết với một sync.Locker.
  2. Khóa Mutex (cond.L.Lock()).
  3. Kiểm tra điều kiện: Nếu điều kiện chưa được đáp ứng, gọi cond.Wait(). Wait sẽ tự động mở khóa Mutex, chặn goroutine, và khi được đánh thức, nó sẽ khóa lại Mutex.
  4. Sau khi điều kiện được đáp ứng, thực hiện các thao tác cần thiết.
  5. Mở khóa Mutex (cond.L.Unlock()).
  6. Thông báo cho các goroutine khác bằng cond.Signal() (đánh thức một goroutine) hoặc cond.Broadcast() (đánh thức tất cả các goroutine đang chờ).
package main

import (
	"fmt"
	"sync"
	"time"
)

var (
	queue   = []int{} // Hàng đợi dữ liệu
	maxSize = 3       // Dung lượng tối đa của hàng đợi
	mu      sync.Mutex
	cond    = sync.NewCond(&mu) // Khởi tạo biến điều kiện với mutex
)

// Producer
func producer(id int) {
	for i := 0; i < 5; i++ {
		mu.Lock() // Khóa trước khi truy cập hàng đợi
		for len(queue) == maxSize {
			fmt.Printf("Producer %d: Hàng đợi đầy, đang chờ...\n", id)
			cond.Wait() // Hàng đợi đầy, chờ consumer
		}
		item := id*10 + i
		queue = append(queue, item)
		fmt.Printf("Producer %d: Đã sản xuất %d, Hàng đợi: %v\n", id, item, queue)
		cond.Signal() // Báo hiệu cho consumer có dữ liệu mới
		mu.Unlock()   // Mở khóa
		time.Sleep(100 * time.Millisecond)
	}
}

// Consumer
func consumer(id int) {
	for i := 0; i < 5; i++ {
		mu.Lock() // Khóa trước khi truy cập hàng đợi
		for len(queue) == 0 {
			fmt.Printf("Consumer %d: Hàng đợi trống, đang chờ...\n", id)
			cond.Wait() // Hàng đợi trống, chờ producer
		}
		item := queue[0]
		queue = queue[1:]
		fmt.Printf("Consumer %d: Đã tiêu thụ %d, Hàng đợi: %v\n", id, item, queue)
		cond.Signal() // Báo hiệu cho producer có chỗ trống mới
		mu.Unlock()   // Mở khóa
		time.Sleep(200 * time.Millisecond)
	}
}

func main() {
	var wg sync.WaitGroup

	wg.Add(2)
	go func() {
		defer wg.Done()
		producer(1)
	}()
	go func() {
		defer wg.Done()
		consumer(1)
	}()

	wg.Wait()
	fmt.Println("Hoàn thành Producer-Consumer với Condition Variables.")
}

Kiến trúc Phân tầng Mạng

Mô hình TCP/IP là một kiến trúc mạng phân tầng được sử dụng rộng rãi.

Mô hình TCP/IP bốn tầng:

  1. Tầng ứng dụng (Application Layer): Tương tác trực tiếp với các ứng dụng người dùng. Các giao thức như HTTP, FTP, DNS.
  2. Tầng vận chuyển (Transport Layer): Cung cấp dịch vụ giao tiếp end-to-end giữa các tiến trình. Các giao thức chính:
    • TCP (Transmission Control Protocol): Giao thức hướng kết nối, đáng tin cậy, truyền dữ liệu theo luồng.
    • UDP (User Datagram Protocol): Giao thức không hướng kết nối, không đáng tin cậy, truyền dữ liệu theo gói tin (datagram).
    • Port: Một số nguyên để nhận diện duy nhất một tiến trình trên một máy chủ (ví dụ: 80 cho HTTP, 443 cho HTTPS).
  3. Tầng mạng (Internet Layer): Chịu trách nhiệm định tuyến gói tin qua mạng. Giao thức chính:
    • IP (Internet Protocol): Cung cấp địa chỉ logic (địa chỉ IP) để xác định duy nhất một thiết bị trên mạng.
  4. Tầng liên kết dữ liệu (Link Layer): Xử lý việc truyền dữ liệu qua một liên kết vật lý cục bộ. Các giao thức như Ethernet, Wi-Fi.
    • MAC (Media Access Control) Address: Địa chỉ vật lý duy nhất của một card mạng.
    • ARP (Address Resolution Protocol): Dùng để ánh xạ địa chỉ IP sang địa chỉ MAC.

Quá trình giao tiếp dữ liệu: Dữ liệu được đóng gói (encapsulation) với các header của từng tầng khi gửi và được giải đóng gói (decapsulation) khi nhận.

Socket: Một điểm cuối (endpoint) của giao tiếp hai chiều giữa hai chương trình trên mạng. Socket luôn xuất hiện theo cặp.

Máy chủ/Ứng dụng TCP Client-Server

Go cung cấp gói net để xây dựng các ứng dụng mạng.

Máy chủ TCP (TCP Server)

  1. Tạo một Listener để lắng nghe kết nối đến: net.Listen("tcp", "IP:Port").
  2. Chấp nhận các kết nối đến bằng listener.Accept(). Mỗi Accept trả về một Conn (kết nối).
  3. Đọc dữ liệu từ Conn bằng conn.Read().
  4. Xử lý dữ liệu.
  5. Ghi dữ liệu phản hồi vào Conn bằng conn.Write().
  6. Đóng ListenerConn khi không còn cần thiết.

Sử dụng công cụ nc (netcat) để kiểm tra: nc 127.0.0.1 8000.

Ứng dụng TCP Client (TCP Client)

  1. Kết nối đến máy chủ: net.Dial("tcp", "Server_IP:Server_Port"). Trả về một Conn.
  2. Ghi dữ liệu vào Conn bằng conn.Write().
  3. Đọc phản hồi từ Conn bằng conn.Read().
  4. Đóng Conn.

Máy chủ TCP đa luồng (Concurrent TCP Server)

Để xử lý nhiều kết nối client đồng thời, máy chủ thường tạo một goroutine mới cho mỗi kết nối:

package main

import (
	"fmt"
	"io"
	"net"
	"strings"
	"time"
)

// handleClientConnection xử lý một kết nối client riêng biệt
func handleClientConnection(conn net.Conn) {
	defer conn.Close() // Đảm bảo đóng kết nối khi goroutine kết thúc
	clientAddr := conn.RemoteAddr().String()
	fmt.Printf("Client %s đã kết nối.\n", clientAddr)

	buffer := make([]byte, 1024)
	for {
		// Đọc dữ liệu từ client
		n, err := conn.Read(buffer)
		if err != nil {
			if err == io.EOF {
				fmt.Printf("Client %s đã ngắt kết nối.\n", clientAddr)
			} else {
				fmt.Printf("Lỗi đọc từ client %s: %v\n", clientAddr, err)
			}
			return // Thoát khỏi vòng lặp khi có lỗi hoặc client ngắt kết nối
		}

		clientMessage := strings.TrimSpace(string(buffer[:n]))
		fmt.Printf("Nhận từ %s: %s\n", clientAddr, clientMessage)

		// Chuyển đổi tin nhắn thành chữ hoa và gửi lại
		response := strings.ToUpper(clientMessage)
		_, err = conn.Write([]byte("SERVER: " + response + "\n"))
		if err != nil {
			fmt.Printf("Lỗi ghi cho client %s: %v\n", clientAddr, err)
			return
		}
	}
}

func main() {
	listenAddr := "127.0.0.1:8000"
	listener, err := net.Listen("tcp", listenAddr)
	if err != nil {
		fmt.Printf("Lỗi khi lắng nghe: %v\n", err)
		return
	}
	defer listener.Close()
	fmt.Printf("Máy chủ TCP đang lắng nghe trên %s\n", listenAddr)

	for {
		conn, err := listener.Accept() // Chờ và chấp nhận kết nối client mới
		if err != nil {
			fmt.Printf("Lỗi chấp nhận kết nối: %v\n", err)
			continue // Tiếp tục lắng nghe các kết nối khác
		}
		go handleClientConnection(conn) // Xử lý mỗi kết nối trong một goroutine riêng
	}
}

Ứng dụng TCP Client đa luồng (Concurrent TCP Client)

Một client có thể sử dụng goroutine để đọc từ bàn phím và gửi đến server, đồng thời đọc phản hồi từ server.

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"strings"
)

func main() {
	serverAddr := "127.0.0.1:8000" // Địa chỉ của máy chủ TCP
	conn, err := net.Dial("tcp", serverAddr)
	if err != nil {
		fmt.Printf("Lỗi kết nối đến máy chủ: %v\n", err)
		return
	}
	defer conn.Close()
	fmt.Printf("Đã kết nối đến máy chủ %s\n", serverAddr)

	// Goroutine để đọc từ server và in ra console
	go func() {
		reader := bufio.NewReader(conn)
		for {
			message, err := reader.ReadString('\n')
			if err != nil {
				fmt.Println("Lỗi đọc từ server hoặc server đã đóng:", err)
				return
			}
			fmt.Print(message)
		}
	}()

	// Đọc từ bàn phím và gửi đến server
	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		text := scanner.Text()
		_, err := fmt.Fprintf(conn, "%s\n", text) // Gửi văn bản + xuống dòng
		if err != nil {
			fmt.Println("Lỗi gửi dữ liệu:", err)
			return
		}
	}
	if err := scanner.Err(); err != nil {
		fmt.Println("Lỗi đọc từ stdin:", err)
	}
}

Quá trình giao tiếp TCP

  • Ba bước bắt tay (Three-way Handshake): Thiết lập kết nối TCP đáng tin cậy.
    1. Client gửi gói tin SYN (Synchronize Sequence Number).
    2. Server nhận SYN, gửi lại SYN-ACK (Synchronize-Acknowledge).
    3. Client nhận SYN-ACK, gửi lại ACK (Acknowledge).
    Kết nối được thiết lập (net.Dial()listener.Accept() hoàn thành).
  • Bốn bước vẫy tay (Four-way Handshake): Kết thúc kết nối TCP.
    1. Bên chủ động đóng (Client/Server) gửi gói tin FIN (Finish).
    2. Bên bị động nhận FIN, gửi lại ACK. (Kết nối được đóng một nửa - half-close).
    3. Bên bị động gửi gói tin FIN của riêng nó.
    4. Bên chủ động gửi lại ACK.
    Kết nối hoàn toàn đóng. Thời gian chờ TIME_WAIT (2MSL) đảm bảo gói ACK cuối cùng được nhận.
  • TCP là giao thức hướng kết nối và đáng tin cậy.

Giao tiếp UDP

UDP là giao thức không hướng kết nối, không đáng tin cậy, truyền dữ liệu theo gói tin (datagram).

Máy chủ UDP (UDP Server)

package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8001")
	if err != nil {
		fmt.Printf("Lỗi resolve địa chỉ UDP: %v\n", err)
		return
	}

	conn, err := net.ListenUDP("udp", serverAddr) // Lắng nghe kết nối UDP
	if err != nil {
		fmt.Printf("Lỗi lắng nghe UDP: %v\n", err)
		return
	}
	defer conn.Close()
	fmt.Printf("Máy chủ UDP đang lắng nghe trên %s\n", serverAddr.String())

	buffer := make([]byte, 1024)
	for {
		n, clientAddr, err := conn.ReadFromUDP(buffer) // Đọc dữ liệu từ client
		if err != nil {
			fmt.Printf("Lỗi đọc từ UDP: %v\n", err)
			continue
		}

		message := string(buffer[:n])
		fmt.Printf("Nhận từ %s: %s\n", clientAddr.String(), message)

		// Gửi phản hồi
		response := fmt.Sprintf("SERVER: Đã nhận '%s' lúc %s", message, time.Now().Format("15:04:05"))
		_, err = conn.WriteToUDP([]byte(response), clientAddr)
		if err != nil {
			fmt.Printf("Lỗi ghi cho UDP client %s: %v\n", clientAddr.String(), err)
		}
	}
}

Ứng dụng UDP Client (UDP Client)

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
	"time"
)

func main() {
	serverAddr, err := net.ResolveUDPAddr("udp", "127.0.0.1:8001")
	if err != nil {
		fmt.Printf("Lỗi resolve địa chỉ UDP: %v\n", err)
		return
	}

	conn, err := net.DialUDP("udp", nil, serverAddr) // Kết nối đến server UDP
	if err != nil {
		fmt.Printf("Lỗi dial UDP: %v\n", err)
		return
	}
	defer conn.Close()
	fmt.Printf("Đã kết nối đến server UDP %s\n", serverAddr.String())

	// Goroutine để đọc phản hồi từ server
	go func() {
		buffer := make([]byte, 1024)
		for {
			n, _, err := conn.ReadFromUDP(buffer) // Đọc phản hồi
			if err != nil {
				fmt.Println("Lỗi đọc phản hồi từ server:", err)
				return
			}
			fmt.Printf("Phản hồi từ server: %s\n", string(buffer[:n]))
		}
	}()

	// Đọc từ bàn phím và gửi tin nhắn
	reader := bufio.NewReader(os.Stdin)
	for {
		fmt.Print("Nhập tin nhắn để gửi: ")
		text, _ := reader.ReadString('\n')
		text = strings.TrimSpace(text)
		if text == "exit" {
			break
		}

		_, err := conn.Write([]byte(text)) // Gửi tin nhắn qua UDP
		if err != nil {
			fmt.Printf("Lỗi gửi tin nhắn: %v\n", err)
			break
		}
		time.Sleep(500 * time.Millisecond) // Đợi phản hồi
	}
	fmt.Println("Client UDP đã thoát.")
}

Truyền file qua mạng

Truyền file thường sử dụng TCP vì tính đáng tin cậy của nó.

Chương trình gửi file (Client)

package main

import (
	"fmt"
	"io"
	"net"
	"os"
	"path/filepath"
	"strings"
)

func sendFile(conn net.Conn, filePath string) error {
	file, err := os.Open(filePath)
	if err != nil {
		return fmt.Errorf("không thể mở file nguồn: %v", err)
	}
	defer file.Close()

	// Gửi tên file trước
	fileName := filepath.Base(filePath)
	_, err = conn.Write([]byte(fileName + "\n"))
	if err != nil {
		return fmt.Errorf("không thể gửi tên file: %v", err)
	}

	// Đợi phản hồi "OK" từ server
	responseBuffer := make([]byte, 1024)
	n, err := conn.Read(responseBuffer)
	if err != nil {
		return fmt.Errorf("không thể đọc phản hồi từ server: %v", err)
	}
	if strings.TrimSpace(string(responseBuffer[:n])) != "OK" {
		return fmt.Errorf("server từ chối nhận file: %s", string(responseBuffer[:n]))
	}

	// Gửi nội dung file
	fmt.Printf("Đang gửi nội dung file '%s'...\n", fileName)
	bytesSent, err := io.Copy(conn, file)
	if err != nil {
		return fmt.Errorf("lỗi khi gửi nội dung file: %v", err)
	}
	fmt.Printf("Đã gửi %d bytes.\n", bytesSent)
	return nil
}

func main() {
	if len(os.Args) < 2 {
		fmt.Println("Cách dùng: go run client.go <đường_dẫn_file>")
		return
	}
	filePath := os.Args[1]

	// Tạo file mẫu nếu không tồn tại
	if _, err := os.Stat(filePath); os.IsNotExist(err) {
		_ = os.WriteFile(filePath, []byte("Đây là nội dung của file mẫu để truyền."), 0644)
		fmt.Printf("Đã tạo file mẫu: %s\n", filePath)
	}

	serverAddr := "127.0.0.1:8002"
	conn, err := net.Dial("tcp", serverAddr)
	if err != nil {
		fmt.Printf("Lỗi kết nối đến server: %v\n", err)
		return
	}
	defer conn.Close()
	fmt.Printf("Đã kết nối đến server %s để gửi file.\n", serverAddr)

	err = sendFile(conn, filePath)
	if err != nil {
		fmt.Printf("Lỗi gửi file: %v\n", err)
	} else {
		fmt.Println("Gửi file hoàn tất.")
	}
}

Chương trình nhận file (Server)

package main

import (
	"bufio"
	"fmt"
	"io"
	"net"
	"os"
	"strings"
	"time"
)

func receiveFile(conn net.Conn) error {
	reader := bufio.NewReader(conn)

	// Đọc tên file
	fileName, err := reader.ReadString('\n')
	if err != nil {
		return fmt.Errorf("không thể đọc tên file: %v", err)
	}
	fileName = strings.TrimSpace(fileName)
	if fileName == "" {
		return fmt.Errorf("tên file trống")
	}

	// Tạo file để ghi
	outputFile, err := os.Create("received_" + fileName)
	if err != nil {
		return fmt.Errorf("không thể tạo file đích: %v", err)
	}
	defer outputFile.Close()
	fmt.Printf("Đang nhận file '%s'...\n", fileName)

	// Gửi phản hồi "OK" cho client
	_, err = conn.Write([]byte("OK\n"))
	if err != nil {
		return fmt.Errorf("không thể gửi phản hồi OK cho client: %v", err)
	}

	// Nhận nội dung file
	bytesReceived, err := io.Copy(outputFile, conn)
	if err != nil {
		return fmt.Errorf("lỗi khi nhận nội dung file: %v", err)
	}
	fmt.Printf("Đã nhận %d bytes vào file 'received_%s'.\n", bytesReceived, fileName)
	return nil
}

func main() {
	listenAddr := "127.0.0.1:8002"
	listener, err := net.Listen("tcp", listenAddr)
	if err != nil {
		fmt.Printf("Lỗi lắng nghe: %v\n", err)
		return
	}
	defer listener.Close()
	fmt.Printf("Server nhận file đang lắng nghe trên %s\n", listenAddr)

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("Lỗi chấp nhận kết nối: %v\n", err)
			continue
		}
		go func(clientConn net.Conn) {
			defer clientConn.Close()
			clientAddr := clientConn.RemoteAddr().String()
			fmt.Printf("Client %s đã kết nối để gửi file.\n", clientAddr)
			err := receiveFile(clientConn)
			if err != nil {
				fmt.Printf("Lỗi xử lý file từ client %s: %v\n", clientAddr, err)
			} else {
				fmt.Printf("Hoàn thành xử lý file từ client %s.\n", clientAddr)
			}
		}(conn)
	}
}

Phòng Chat (Chat Room)

Một ví dụ phức tạp hơn về ứng dụng mạng, sử dụng goroutine và channel để quản lý nhiều người dùng và tin nhắn.

Cấu trúc cơ bản:

  • Một goroutine chính lắng nghe kết nối client.
  • Mỗi client kết nối được xử lý bởi một goroutine riêng (handleClientConnection).
  • Một goroutine quản lý trung tâm (manager) xử lý các tin nhắn từ client và phát sóng đến tất cả người dùng trực tuyến.
  • Các channel được sử dụng để giao tiếp giữa các goroutine (ví dụ: gửi tin nhắn client đến manager, thông báo user online/offline).
package main

import (
	"bufio"
	"fmt"
	"net"
	"strings"
	"sync"
	"time"
)

// User đại diện cho một người dùng trong phòng chat
type User struct {
	ID        string
	Name      string
	Conn      net.Conn
	MessageCh chan string // Kênh để gửi tin nhắn riêng cho người dùng này
	LastActive time.Time
}

var (
	users      = make(map[string]*User) // Map lưu trữ người dùng trực tuyến (ID -> User)
	usersMutex sync.RWMutex             // Mutex để bảo vệ map users
	joinCh     = make(chan *User)       // Kênh cho người dùng mới tham gia
	leaveCh    = make(chan *User)       // Kênh cho người dùng rời đi
	messageCh  = make(chan string)      // Kênh cho tất cả tin nhắn để phát sóng
)

// manager Goroutine quản lý trạng thái phòng chat và phát sóng tin nhắn
func manager() {
	for {
		select {
		case newUser := <-joinCh:
			usersMutex.Lock()
			users[newUser.ID] = newUser
			usersMutex.Unlock()
			broadcastMessage(fmt.Sprintf("%s đã tham gia phòng chat.", newUser.Name))
			sendUserList(newUser) // Gửi danh sách người dùng cho người dùng mới
		case leavingUser := <-leaveCh:
			usersMutex.Lock()
			delete(users, leavingUser.ID)
			usersMutex.Unlock()
			broadcastMessage(fmt.Sprintf("%s đã rời phòng chat.", leavingUser.Name))
		case msg := <-messageCh:
			broadcastMessage(msg)
		}
	}
}

// broadcastMessage gửi tin nhắn đến tất cả người dùng trực tuyến
func broadcastMessage(msg string) {
	usersMutex.RLock()
	defer usersMutex.RUnlock()
	for _, user := range users {
		select {
		case user.MessageCh <- msg:
		default:
			// Nếu kênh tin nhắn của người dùng đầy, bỏ qua tin nhắn
			fmt.Printf("Cảnh báo: Kênh tin nhắn của %s đầy, không thể gửi: %s\n", user.Name, msg)
		}
	}
}

// sendUserList gửi danh sách người dùng cho một người dùng cụ thể
func sendUserList(targetUser *User) {
	usersMutex.RLock()
	defer usersMutex.RUnlock()
	list := []string{}
	for _, u := range users {
		list = append(list, u.Name)
	}
	targetUser.MessageCh <- fmt.Sprintf("Người dùng trực tuyến (%d): %s", len(list), strings.Join(list, ", "))
}

// handleClientConnection xử lý giao tiếp với từng client
func handleClientConnection(conn net.Conn) {
	defer conn.Close()

	reader := bufio.NewReader(conn)
	conn.Write([]byte("Chào mừng đến với phòng chat! Vui lòng nhập tên của bạn: \n"))

	name, err := reader.ReadString('\n')
	if err != nil {
		fmt.Println("Lỗi đọc tên:", err)
		return
	}
	name = strings.TrimSpace(name)
	if name == "" {
		name = fmt.Sprintf("Người dùng ẩn danh_%d", time.Now().UnixNano()%1000)
	}

	userID := conn.RemoteAddr().String()
	currentUser := &User{
		ID:        userID,
		Name:      name,
		Conn:      conn,
		MessageCh: make(chan string, 100), // Kênh tin nhắn cho người dùng này
		LastActive: time.Now(),
	}

	joinCh <- currentUser // Gửi thông báo người dùng tham gia

	// Goroutine để gửi tin nhắn từ kênh của người dùng đến client
	go func() {
		for msg := range currentUser.MessageCh {
			_, err := currentUser.Conn.Write([]byte(msg + "\n"))
			if err != nil {
				fmt.Printf("Lỗi gửi tin nhắn cho %s: %v\n", currentUser.Name, err)
				return // Thoát khỏi goroutine gửi
			}
		}
	}()

	// Thời gian chờ cho hoạt động của người dùng (ví dụ: 5 phút)
	inactivityTimeout := 5 * time.Minute
	timeoutCh := time.After(inactivityTimeout)

	// Đọc tin nhắn từ client
	for {
		select {
		case <-timeoutCh:
			currentUser.MessageCh <- fmt.Sprintf("Bạn đã bị ngắt kết nối do không hoạt động trong %s.", inactivityTimeout)
			fmt.Printf("Người dùng %s đã bị ngắt kết nối do không hoạt động.\n", currentUser.Name)
			leaveCh <- currentUser
			return // Ngắt kết nối
		default:
			conn.SetReadDeadline(time.Now().Add(1 * time.Second)) // Đặt thời gian chờ đọc ngắn
			message, err := reader.ReadString('\n')
			if err != nil {
				if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
					// Lỗi timeout khi đọc, tiếp tục kiểm tra các trường hợp khác
					continue
				}
				fmt.Printf("Client %s (%s) đã ngắt kết nối: %v\n", currentUser.Name, currentUser.ID, err)
				leaveCh <- currentUser // Gửi thông báo người dùng rời đi
				return
			}

			message = strings.TrimSpace(message)
			currentUser.LastActive = time.Now() // Cập nhật thời gian hoạt động

			// Xử lý các lệnh chat
			if strings.HasPrefix(message, "/name ") {
				newName := strings.TrimPrefix(message, "/name ")
				newName = strings.TrimSpace(newName)
				if newName != "" {
					oldName := currentUser.Name
					currentUser.Name = newName
					usersMutex.Lock()
					users[currentUser.ID] = currentUser // Cập nhật trong map
					usersMutex.Unlock()
					broadcastMessage(fmt.Sprintf("%s đã đổi tên thành %s.", oldName, newName))
				} else {
					currentUser.MessageCh <- "Tên mới không hợp lệ."
				}
			} else if message == "/who" {
				sendUserList(currentUser)
			} else {
				// Gửi tin nhắn thông thường để phát sóng
				messageCh <- fmt.Sprintf("%s: %s", currentUser.Name, message)
			}
			timeoutCh = time.After(inactivityTimeout) // Reset timeout sau mỗi hoạt động
		}
	}
}

func main() {
	go manager() // Khởi chạy goroutine quản lý phòng chat

	listenAddr := "127.0.0.1:8003"
	listener, err := net.Listen("tcp", listenAddr)
	if err != nil {
		fmt.Printf("Lỗi lắng nghe: %v\n", err)
		return
	}
	defer listener.Close()
	fmt.Printf("Server phòng chat đang lắng nghe trên %s\n", listenAddr)

	for {
		conn, err := listener.Accept()
		if err != nil {
			fmt.Printf("Lỗi chấp nhận kết nối: %v\n", err)
			continue
		}
		go handleClientConnection(conn) // Xử lý mỗi client trong một goroutine riêng
	}
}

Gói net/http và máy chủ Web

Go có gói net/http tích hợp sẵn để xây dựng các máy chủ và client HTTP.

1. Tạo máy chủ Web đơn giản

package main

import (
	"fmt"
	"net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Chào mừng đến với trang Go!") // Ghi phản hồi HTTP
}

func greetHandler(w http.ResponseWriter, r *http.Request) {
	name := r.URL.Query().Get("name") // Lấy tham số 'name' từ URL
	if name == "" {
		name = "Khách"
	}
	fmt.Fprintf(w, "Xin chào, %s!", name)
}

func main() {
	// Đăng ký hàm xử lý cho các đường dẫn
	http.HandleFunc("/", helloHandler)       // Xử lý yêu cầu đến đường dẫn gốc
	http.HandleFunc("/greet", greetHandler) // Xử lý yêu cầu đến /greet

	listenAddr := "127.0.0.1:8080"
	fmt.Printf("Server HTTP đang chạy trên http://%s\n", listenAddr)
	// Khởi động server HTTP
	err := http.ListenAndServe(listenAddr, nil) // nil nghĩa là sử dụng DefaultServeMux
	if err != nil {
		fmt.Printf("Lỗi khởi động server: %v\n", err)
	}
}

Truy cập http://127.0.0.1:8080/http://127.0.0.1:8080/greet?name=Alice trong trình duyệt.

Web Crawler (Trình thu thập dữ liệu Web)

Một web crawler là một chương trình tự động duyệt Internet, thu thập thông tin từ các trang web.

Quy trình cơ bản:

  1. Xác định URL mục tiêu.
  2. Gửi yêu cầu HTTP đến URL đó và nhận phản hồi.
  3. Phân tích cú pháp phản hồi (thường là HTML) để trích xuất thông tin hữu ích và các URL mới.
  4. Lưu trữ hoặc xử lý dữ liệu đã trích xuất.
  5. Lặp lại quy trình với các URL mới tìm được.

Ví dụ thu thập dữ liệu cơ bản

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"regexp"
	"strings"
	"sync"
	"time"
)

// fetchPageContent gửi yêu cầu GET và trả về nội dung trang
func fetchPageContent(url string) (string, error) {
	fmt.Printf("Đang tải: %s\n", url)
	resp, err := http.Get(url)
	if err != nil {
		return "", fmt.Errorf("lỗi khi tải URL %s: %v", url, err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("URL %s trả về trạng thái lỗi: %d %s", url, resp.StatusCode, resp.Status)
	}

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", fmt.Errorf("lỗi khi đọc nội dung phản hồi từ %s: %v", url, err)
	}
	return string(bodyBytes), nil
}

// saveContentToFile lưu nội dung vào một file
func saveContentToFile(filename, content string) error {
	file, err := os.Create(filename)
	if err != nil {
		return fmt.Errorf("không thể tạo file %s: %v", filename, err)
	}
	defer file.Close()
	_, err = file.WriteString(content)
	if err != nil {
		return fmt.Errorf("không thể ghi vào file %s: %v", filename, err)
	}
	fmt.Printf("Đã lưu nội dung vào: %s\n", filename)
	return nil
}

// extractLinks tìm các URL từ nội dung HTML
func extractLinks(htmlContent string) []string {
	// Biểu thức chính quy đơn giản để tìm các thẻ <a> với href
	// (?s:.*?) để khớp với nội dung nhiều dòng
	re := regexp.MustCompile(`(?s:]*?\s+)?href="([^"]*)")`)
	matches := re.FindAllStringSubmatch(htmlContent, -1)
	var links []string
	for _, match := range matches {
		if len(match) > 1 {
			link := match[1]
			// Chỉ thêm các link có vẻ hợp lệ và không phải là #neo
			if strings.HasPrefix(link, "http") || strings.HasPrefix(link, "/") {
				if !strings.Contains(link, "#") {
					links = append(links, link)
				}
			}
		}
	}
	return links
}

// spiderPage thu thập dữ liệu từ một trang duy nhất
func spiderPage(url string, pageNum int, wg *sync.WaitGroup) {
	defer wg.Done()

	content, err := fetchPageContent(url)
	if err != nil {
		fmt.Printf("Lỗi thu thập dữ liệu trang %s: %v\n", url, err)
		return
	}

	filename := fmt.Sprintf("page_%d.html", pageNum)
	err = saveContentToFile(filename, content)
	if err != nil {
		fmt.Printf("Lỗi lưu file: %v\n", err)
	}

	// Ví dụ: tìm các link mới (có thể mở rộng để tiếp tục thu thập)
	// foundLinks := extractLinks(content)
	// fmt.Printf("Tìm thấy %d liên kết trên trang %d\n", len(foundLinks), pageNum)
	// for _, link := range foundLinks {
	//     fmt.Println("  - " + link)
	// }
}

func main() {
	baseURL := "https://www.example.com" // Thay thế bằng URL thực tế
	numPagesToCrawl := 3

	var wg sync.WaitGroup
	fmt.Println("Bắt đầu Web Crawler...")

	for i := 0; i < numPagesToCrawl; i++ {
		pageURL := fmt.Sprintf("%s/page/%d", baseURL, i+1) // Tạo URL cho các trang giả định
		wg.Add(1)
		go spiderPage(pageURL, i+1, &wg) // Khởi chạy goroutine cho mỗi trang
		time.Sleep(100 * time.Millisecond) // Tránh quá tải server
	}

	wg.Wait() // Chờ tất cả goroutine hoàn thành
	fmt.Println("Web Crawler hoàn tất.")
}

Sử dụng Biểu thức chính quy (Regular Expressions)

Gói regexp trong Go cho phép làm việc với biểu thức chính quy để tìm kiếm và trích xuất dữ liệu.

  • regexp.MustCompile(pattern string) *Regexp: Biên dịch một biểu thức chính quy. Nếu có lỗi, nó sẽ panic.
  • regexp.MustCompile(pattern string) *Regexp: Biên dịch một biểu thức chính quy. Nếu có lỗi, nó sẽ panic.
  • re.FindAllStringSubmatch(s string, n int) [][]string: Tìm tất cả các kết quả khớp trong chuỗi s. n là số lượng kết quả tối đa (-1 để tìm tất cả). Trả về một slice chứa các slice chuỗi, với match[0] là toàn bộ kết quả khớp và match[1], match[2], ... là các nhóm con (capturing groups).
  • (?s:.*?): Một pattern hữu ích để khớp với nội dung nhiều dòng. (?s) bật cờ "dotall" (dấu chấm khớp với ký tự ngắt dòng), và .*? là non-greedy match.

Thu thập dữ liệu hai chiều

  • Theo chiều ngang (Horizontal): Thu thập từng trang web theo thứ tự (ví dụ: trang 1, trang 2, ...).
  • Theo chiều dọc (Vertical): Thu thập tất cả các mục hoặc thông tin chi tiết trên một trang cụ thể.

Thẻ: Go golang Programming Concurrency Channels

Đăng vào ngày 16 tháng 9 lúc 20:12