1. Go là gì?

Go (Golang) là ngôn ngữ lập trình go thường được sử dụng trong backend, REST API, hệ thống mạng, công cụ dòng lệnh, cloud infrastructure và các dịch vụ chạy đồng thời.

Go là ngôn ngữ biên dịch và có hệ thống kiểu dữ liệu tĩnh (statically typed). Điều này có nghĩa chương trình được biên dịch trước khi chạy, đồng thời nhiều lỗi liên quan đến kiểu dữ liệu có thể được phát hiện ngay trong quá trình biên dịch.

Ví dụ:

package main

import "fmt"

func main() {
    var age int = 20
    fmt.Println(age)
}

Một đặc điểm quan trọng khác của Go là ngôn ngữ được thiết kế tương đối gọn. Go không xây dựng hệ thống class, inheritance và exception theo cách giống Java hoặc PHP. Thay vào đó, Go sử dụng struct, method, interface và cơ chế trả về error để tổ chức chương trình.

Go cũng có hỗ trợ concurrency ở cấp ngôn ngữ thông qua goroutine và channel. Đây là một lý do Go phù hợp với những ứng dụng backend cần xử lý nhiều kết nối, request hoặc công việc đồng thời.

2. Package, Import và Export

Mỗi file Go bắt đầu bằng khai báo package. Ví dụ:

package main

Package main có ý nghĩa đặc biệt: khi chương trình được xây dựng thành executable, package này cần có function main() làm điểm bắt đầu thực thi.

Để sử dụng chức năng từ package khác, Go dùng import:

import "fmt"

Sau đó có thể gọi các function hoặc sử dụng những kiểu được package đó cung cấp:

fmt.Println("Hello")

fmt là package trong standard library, cung cấp các chức năng định dạng và hiển thị dữ liệu.

Go có một quy tắc quan trọng về khả năng truy cập: tên bắt đầu bằng chữ hoa được export ra ngoài package; tên bắt đầu bằng chữ thường chỉ có thể được sử dụng trực tiếp trong package khai báo nó.

Ví dụ:

package user

type User struct {
    Name  string
    email string
}

func NewUser(name string) User {
    return User{Name: name}
}

Package khác có thể sử dụng user.User, Name và user.NewUser(). Tuy nhiên, trường email không được truy cập trực tiếp từ bên ngoài package user.

3. Function và Return Value

Một function trong Go được khai báo bằng từ khóa func:

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

Khi các parameter liên tiếp có cùng kiểu, có thể viết gọn:

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

Function trên nhận hai số nguyên và trả về một số nguyên.

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("cannot divide by zero")
    }

    return a / b, nil
}

Khi gọi function:

result, err := divide(10, 2)

if err != nil {
    fmt.Println("Error:", err)
    return
}
fmt.Println(result)

Cách làm này thể hiện rõ một nguyên tắc của Go: kết quả xử lý và lỗi thường được truyền qua giá trị trả về, giúp bên gọi chủ động quyết định cần làm gì khi có lỗi.

Ngoài ra, Go còn có variadic function, cho phép nhận một số lượng tham số thay đổi:

func sum(numbers ...int) int {
    total := 0

    for _, number := range numbers {
        total += number
    }

    return total
}

Có thể gọi sum(1, 2, 3) hoặc sum(1, 2, 3, 4, 5). Tính năng này hữu ích với những function như tính tổng, định dạng hoặc ghi log.

Function cũng có thể được gán vào biến hoặc truyền như tham số. Điều này cho phép Go sử dụng higher-order function và closure khi cần, dù không nên đưa các kỹ thuật này vào code nếu chỉ khiến logic phức tạp hơn.

4. Array, Slice và Map

Array

Array có kích thước cố định được xác định trong kiểu dữ liệu:

var numbers [3]int = [3]int{10, 20, 30}

Array phù hợp khi số lượng phần tử là cố định và kích thước là một phần của kiểu dữ liệu. Tuy nhiên, trong code ứng dụng thông thường, slice xuất hiện thường xuyên hơn.

Slice

Slice là cấu trúc dữ liệu linh hoạt, cung cấp một góc nhìn lên một vùng dữ liệu của array nền. Slice có thể thay đổi độ dài trong giới hạn capacity, và append có thể tạo vùng lưu trữ mới khi cần.

numbers := []int{10, 20, 30}

numbers = append(numbers, 40)

fmt.Println(numbers)

Một chi tiết dễ gây lỗi là nhiều slice có thể cùng tham chiếu đến một array nền. Vì vậy, sửa phần tử thông qua một slice đôi khi có thể ảnh hưởng đến dữ liệu nhìn thấy qua slice khác. Khi chia sẻ hoặc sao chép dữ liệu, cần hiểu rõ hành vi này.

Nếu cần tạo bản sao độc lập của các phần tử:

original := []int{1, 2, 3}

copied := make([]int, len(original))
copy(copied, original)

Slice thường được dùng để xử lý danh sách user, product, order hoặc dữ liệu trả về từ database.

Map

Map lưu dữ liệu theo cặp key-value:

user := map[string]string{
    "name":  "John",
    "email": "[email protected]",
}

Truy cập dữ liệu:

name := user["name"]

Nếu cần biết một key có tồn tại hay không, dùng cú pháp kiểm tra giá trị thứ hai:

email, exists := user["email"]

if exists {
    fmt.Println(email)
}

Map không đảm bảo thứ tự khi duyệt. Vì vậy, không nên dựa vào thứ tự của phần tử khi chạy range trên map.

Map cũng không tự an toàn khi nhiều goroutine đồng thời ghi hoặc đọc/ghi dữ liệu có thay đổi. Trong trường hợp chia sẻ map giữa nhiều goroutine, cần có cơ chế đồng bộ phù hợp, chẳng hạn mutex hoặc một thiết kế concurrency khác.

5. Interface – Một trong những khái niệm quan trọng nhất của Go

Interface mô tả những hành vi mà một kiểu dữ liệu cần cung cấp, chứ không nhất thiết mô tả cấu trúc dữ liệu bên trong.

Ví dụ:

type Notifier interface {
    Send(message string) error
}

Một kiểu dữ liệu đáp ứng interface này nếu nó có method Send(string) error phù hợp. Go không yêu cầu class phải khai báo từ khóa implements như trong một số ngôn ngữ khác.

type EmailNotifier struct{}

func (e EmailNotifier) Send(message string) error {
    fmt.Println("Email:", message)
    return nil
}

EmailNotifier tự động thỏa mãn interface Notifier vì có method phù hợp.

Điều này hữu ích khi application cần sử dụng nhiều implementation khác nhau cho cùng một hành vi. Ví dụ hệ thống thông báo có thể sử dụng email, SMS hoặc push notification, trong khi phần nghiệp vụ chỉ phụ thuộc vào interface chung.

Một nguyên tắc thường gặp trong Go là interface nên nhỏ và phản ánh hành vi thực sự mà bên sử dụng cần. Không nên tạo interface cho mọi struct chỉ vì muốn code trông có vẻ trừu tượng hơn.

Interface cũng giúp viết unit test dễ dàng hơn. Một service có thể nhận dependency dưới dạng interface, rồi test bằng một implementation giả mà không cần gọi dịch vụ bên ngoài thực sự.

6. Concurrency – Goroutine, Channel và Synchronization

Concurrency là một trong những tính năng nổi bật của Go. Nó cho phép chương trình tổ chức nhiều công việc để có thể tiến triển đồng thời, thay vì buộc toàn bộ thao tác phải thực hiện tuần tự.

Cần phân biệt concurrency và parallelism. Concurrency nói về cách tổ chức nhiều công việc đang diễn ra; parallelism nói về việc thực sự thực thi nhiều công việc cùng lúc. Một chương trình concurrent không nhất thiết luôn chạy song song trên nhiều CPU.

Goroutine

Goroutine là đơn vị thực thi nhẹ do Go runtime quản lý. Có thể khởi chạy một function dưới dạng goroutine bằng từ khóa go:

go processOrder()

Cú pháp này cho phép công việc bắt đầu thực thi đồng thời với phần code tiếp tục chạy ở goroutine gọi.

Goroutine thường được dùng trong xử lý request, tác vụ nền hoặc công việc I/O có thể tiến triển đồng thời. Tuy nhiên, khởi chạy goroutine không có nghĩa công việc sẽ tự động hoàn thành trước khi chương trình kết thúc. Khi cần chờ kết quả, cần sử dụng cơ chế đồng bộ phù hợp.

Channel

Channel là cơ chế giao tiếp và đồng bộ giữa các goroutine.

messages := make(chan string)

go func() {
    messages <- "task completed"
}()

message := <-messages
fmt.Println(message)

Channel không đệm như trong ví dụ sẽ đồng bộ thao tác gửi và nhận: bên gửi có thể phải chờ cho đến khi có bên nhận phù hợp.

Channel có thể hữu ích khi một goroutine tạo ra kết quả và chuyển kết quả cho goroutine khác, hoặc khi cần phối hợp nhiều công việc. Nhưng không phải mọi bài toán concurrency đều cần channel.

Mutex và WaitGroup

Khi nhiều goroutine cần truy cập cùng một dữ liệu mutable, có thể dùng sync.Mutex để kiểm soát vùng code truy cập dữ liệu đó.

sync.WaitGroup giúp chờ một nhóm công việc được đánh dấu hoàn thành. Đây là công cụ phù hợp khi cần đợi các goroutine kết thúc trước khi tiếp tục.

Điểm quan trọng là phải thiết kế rõ thời điểm khởi chạy, chờ, hủy và đồng bộ công việc. Nếu nhiều goroutine đọc/ghi dữ liệu không có đồng bộ phù hợp, application có thể xuất hiện race condition hoặc lỗi khó tái hiện.

Context

Trong backend, context.Context rất quan trọng khi xử lý request hoặc công việc có thể bị hủy. Context thường được dùng để truyền deadline, tín hiệu cancellation và một số metadata theo phạm vi công việc.

Ví dụ, khi client đóng kết nối hoặc request hết thời gian cho phép, những thao tác phụ thuộc vào request nên có khả năng dừng theo context khi phù hợp.

Đừng dùng goroutine chỉ vì có thể. Với xử lý đơn giản và tuần tự, code bình thường thường dễ hiểu hơn. Concurrency nên được bổ sung khi có lý do rõ ràng và phải đi kèm cách quản lý vòng đời công việc.

 

Bài viết khác

Go Channels & Go Context

1. Go Channels & Go Context là gì? Trong Go, goroutine cho phép chương trình chạy nhiều công việc đồng thời. Tuy nhiên, khi có nhiều goroutine cùng hoạt động, chúng cần một cách để trao đổi kết quả, phối hợp thời điểm thực thi và dừng công việc khi không còn cần thiết.  Đây […]

Goroutine & Concurrency

1. Concurrency là gì? Concurrency là khả năng tổ chức nhiều công việc để chúng có thể tiến triển xen kẽ hoặc đồng thời trong cùng một khoảng thời gian. Thay vì phải thực hiện tất cả công việc theo một trình tự duy nhất, chương trình có thể chuyển đổi giữa nhiều công việc […]

Laravel Routing & Controllers

1. Laravel Routing & Controllers là gì? Khi xây dựng backend bằng Laravel, client gửi HTTP request đến server để thực hiện một thao tác như lấy danh sách user, xem chi tiết sản phẩm hoặc tạo đơn hàng. Laravel cần xác định request đó sẽ được xử lý ở đâu và logic xử lý […]

Series 2 — Frontend Fundamentals: Responsive Web Design – Bootstrap

Series 2 — Frontend Fundamentals   HTML → Sematic HTML → Forms & Validation → CSS  Flexbox & Grid → Responsive Web Design → Bootstrap   Bài 6. Responsive Web Design – Giao diện đa thiết bị 1. Responsive Web Design là gì? Khi xây dựng một website, chúng ta không thể giả định rằng […]

HTTP & HTTPS

Giao thức HTTP và HTTPS là gì? 1. HTTP là gì? HTTP (Hypertext Transfer Protocol) là giao thức truyền tải siêu văn bản, được sử dụng để trao đổi dữ liệu giữa client (máy khách) và server (máy chủ). Ban đầu, HTTP được phát triển để trao đổi các tài liệu trên Web. Ngày nay, […]

Series 2 — Frontend Fundamentals: CSS — Flexbox-Grid

Series 2 — Frontend Fundamentals   HTML → Sematic HTML → Forms & Validation → CSS  Flexbox & Grid → Responsive Web Design → Bootstrap   Bài 4. CSS Fundamentals – CSS là gì?   CSS Fundamentals – CSS là gì và hoạt động như thế nào? Ở những bài trước, chúng ta đã tìm […]

Leave a Reply

Your email address will not be published. Required fields are marked *