Back to blog

So Sánh Pointer Go và C++: Hướng Dẫn Chi Tiết

gogolangcppc++pointersmemory-management
So Sánh Pointer Go và C++: Hướng Dẫn Chi Tiết

Nếu bạn đang chuyển từ C++ sang Go (hoặc ngược lại), một trong những điều đầu tiên bạn sẽ nhận thấy là cách hai ngôn ngữ này xử lý pointer khác nhau như thế nào. Mặc dù cả hai đều có pointer, nhưng chúng khác biệt đáng kể về triết lý, độ an toàn và khả năng.

Hướng dẫn này cung cấp một so sánh toàn diện để giúp bạn hiểu các khác biệt chính và viết code tốt hơn trong cả hai ngôn ngữ.

Bạn Sẽ Học Được

✅ Khác biệt cú pháp cơ bản giữa pointer Go và C++
✅ Tại sao Go loại bỏ pointer arithmetic (và bạn được lợi gì)
✅ Quản lý bộ nhớ: thủ công vs garbage collection
✅ Null safety và xử lý nil
✅ References trong C++ vs pointers trong Go
✅ Smart pointers trong C++ và tương đương trong Go
✅ Best practices cho cả hai ngôn ngữ


Cú Pháp Pointer Cơ Bản

Hãy bắt đầu với những điều cơ bản. Cả hai ngôn ngữ đều sử dụng * để dereference và & để lấy địa chỉ, nhưng có những khác biệt tinh tế.

Pointer Cơ Bản trong Go

package main
 
import "fmt"
 
func main() {
    // Khai báo và khởi tạo
    x := 42
    var p *int = &x    // p giữ địa chỉ của x
 
    fmt.Println("Giá trị:", x)      // 42
    fmt.Println("Địa chỉ:", &x)     // 0xc000018090 (ví dụ)
    fmt.Println("Pointer:", p)      // 0xc000018090
    fmt.Println("Deref:", *p)       // 42
 
    // Sửa đổi thông qua pointer
    *p = 100
    fmt.Println("x mới:", x)        // 100
 
    // Giá trị zero là nil
    var nilPtr *int
    fmt.Println("nil:", nilPtr)     // <nil>
 
    // new() cấp phát và trả về pointer
    ptr := new(int)
    fmt.Println("new:", *ptr)       // 0 (giá trị zero)
}

Pointer Cơ Bản trong C++

#include <iostream>
 
int main() {
    // Khai báo và khởi tạo
    int x = 42;
    int* p = &x;    // p giữ địa chỉ của x
 
    std::cout << "Giá trị: " << x << std::endl;      // 42
    std::cout << "Địa chỉ: " << &x << std::endl;     // 0x7ffee...
    std::cout << "Pointer: " << p << std::endl;      // 0x7ffee...
    std::cout << "Deref: " << *p << std::endl;       // 42
 
    // Sửa đổi thông qua pointer
    *p = 100;
    std::cout << "x mới: " << x << std::endl;        // 100
 
    // nullptr (C++11+) hoặc NULL
    int* nullPtr = nullptr;
    std::cout << "nullptr: " << nullPtr << std::endl;  // 0
 
    // new cấp phát trên heap (phải delete!)
    int* ptr = new int(0);
    std::cout << "new: " << *ptr << std::endl;       // 0
    delete ptr;  // Bắt buộc!
 
    return 0;
}

Khác Biệt Cú Pháp Chính:

Khía cạnhGoC++
Khai báovar p *intint* p hoặc int *p
Giá trị nullnilnullptr (C++11) hoặc NULL
Cấp phát heapnew(int)new int
Giải phóngTự động (GC)Thủ công (delete)
Vị trí typeSau *Trước *

Pointer Arithmetic: Khác Biệt Lớn Nhất

Đây là sự khác biệt quan trọng nhất giữa pointer Go và C++.

C++ Cho Phép Pointer Arithmetic

#include <iostream>
 
int main() {
    int arr[] = {10, 20, 30, 40, 50};
    int* p = arr;  // Trỏ đến phần tử đầu tiên
 
    std::cout << "p[0]: " << *p << std::endl;       // 10
    std::cout << "p[1]: " << *(p + 1) << std::endl; // 20
    std::cout << "p[2]: " << *(p + 2) << std::endl; // 30
 
    // Tăng pointer
    p++;
    std::cout << "*p sau p++: " << *p << std::endl;  // 20
 
    // Trừ pointer
    int* end = arr + 5;
    std::cout << "Kích thước mảng: " << (end - arr) << std::endl;  // 5
 
    // Nguy hiểm: Không kiểm tra giới hạn!
    int* danger = arr + 100;  // Compile bình thường, undefined behavior
    // std::cout << *danger;  // Có thể crash hoặc trả về rác
 
    return 0;
}

Go Cấm Pointer Arithmetic

package main
 
import "fmt"
 
func main() {
    arr := [5]int{10, 20, 30, 40, 50}
    p := &arr[0]
 
    fmt.Println("p[0]:", *p)  // 10
 
    // Những dòng này KHÔNG compile được:
    // p++                    // Error: invalid operation
    // p = p + 1              // Error: invalid operation
    // q := p + 2             // Error: invalid operation
 
    // Thay vào đó, dùng slices và indices
    slice := arr[:]
    for i, v := range slice {
        fmt.Printf("slice[%d] = %d\n", i, v)
    }
 
    // Truy cập phần tử cụ thể
    fmt.Println("Phần tử 2:", slice[2])  // 30
}

Tại Sao Go Chọn Như Vậy:

Pointer Arithmetic C++Giải Pháp Thay Thế của Go
Lỗi bộ nhớ (buffer overflows)Ngăn chặn compile-time
Undefined behaviorKiểm tra giới hạn với slices
Duyệt mảng thủ côngRange-based iteration
Biểu thức pointer phức tạpCode đơn giản, an toàn hơn

Bối Cảnh Lịch Sử: Pointer arithmetic là nguồn gốc của vô số lỗ hổng bảo mật (buffer overflows, use-after-free, v.v.). Các nhà thiết kế Go đã chọn loại bỏ hoàn toàn class bug này bằng cách không cho phép pointer arithmetic.

Nếu bạn thực sự cần thao tác bộ nhớ low-level trong Go, hãy dùng package unsafe:

import "unsafe"
 
// Điều này không được khuyến khích nhưng có thể làm được
func unsafeIncrement(p *int) *int {
    return (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + unsafe.Sizeof(*p)))
}

Quản Lý Bộ Nhớ

Đây là nơi Go và C++ có cách tiếp cận hoàn toàn khác nhau.

C++: Quản Lý Bộ Nhớ Thủ Công

#include <iostream>
 
class Resource {
public:
    Resource() { std::cout << "Đã tạo" << std::endl; }
    ~Resource() { std::cout << "Đã hủy" << std::endl; }
};
 
void demonstrateManualMemory() {
    // Cấp phát stack (tự động dọn dẹp)
    Resource stackObj;
 
    // Cấp phát heap (phải dọn dẹp thủ công)
    Resource* heapObj = new Resource();
 
    // Nếu quên dòng này, bạn có memory leak!
    delete heapObj;
 
    // Cấp phát mảng
    int* arr = new int[100];
    // Phải dùng delete[] cho mảng!
    delete[] arr;
}
 
// Các vấn đề bộ nhớ phổ biến trong C++
void memoryProblems() {
    int* p = new int(42);
 
    // Vấn đề 1: Quên delete (memory leak)
    // delete p;  // Ôi, quên mất!
 
    // Vấn đề 2: Double delete
    // delete p;
    // delete p;  // Crash hoặc undefined behavior!
 
    // Vấn đề 3: Use after delete
    delete p;
    // *p = 10;  // Undefined behavior!
 
    // Vấn đề 4: Dangling pointer
    int* dangling;
    {
        int local = 42;
        dangling = &local;
    }
    // *dangling bây giờ là rác!
}

Go: Garbage Collection

package main
 
import "fmt"
 
type Resource struct {
    data string
}
 
func demonstrateGC() {
    // Tất cả cấp phát đều được garbage collector theo dõi
 
    // Cấp phát stack (compiler quyết định)
    local := Resource{data: "stack"}
 
    // Cấp phát heap (compiler quyết định qua escape analysis)
    heapPtr := &Resource{data: "heap"}
 
    // Không cần delete - GC xử lý dọn dẹp
    _ = local
    _ = heapPtr
 
    // Trả về pointer đến biến local là AN TOÀN trong Go!
    ptr := createResource()
    fmt.Println(ptr.data)  // Hoạt động tốt
}
 
func createResource() *Resource {
    local := Resource{data: "escaped to heap"}
    return &local  // An toàn! Escape analysis của Go chuyển nó lên heap
}
 
func main() {
    demonstrateGC()
 
    // Không memory leaks, không double-free, không use-after-free
    // GC xử lý mọi thứ tự động
}

So Sánh:

Khía cạnhC++Go
Cấp phátnew/mallocnew/make/literals
Giải phóngdelete/free (thủ công)Tự động (GC)
Memory leaksCó thể xảy raHiếm (chỉ reference cycles)
Double-freeCó thể xảy raKhông thể
Use-after-freeCó thể xảy raKhông thể
Dangling pointersCó thể xảy raKhông thể
Hiệu suấtXác địnhGC pauses (thường <1ms)
Kiểm soátHoàn toànHạn chế

Null Safety

Cả hai ngôn ngữ đều có null/nil pointer, nhưng xử lý khác nhau.

Xử Lý Null trong C++

#include <iostream>
#include <optional>  // C++17
 
void processPointer(int* p) {
    // Phải kiểm tra null thủ công
    if (p != nullptr) {
        std::cout << "Giá trị: " << *p << std::endl;
    } else {
        std::cout << "Null pointer!" << std::endl;
    }
}
 
// C++17: std::optional cho giá trị có thể null
std::optional<int> findValue(int id) {
    if (id > 0) {
        return id * 10;
    }
    return std::nullopt;
}
 
int main() {
    int* null = nullptr;
 
    // Dereference null = undefined behavior!
    // std::cout << *null;  // Crash hoặc tệ hơn
 
    processPointer(null);
 
    // Sử dụng optional
    auto result = findValue(5);
    if (result.has_value()) {
        std::cout << "Tìm thấy: " << result.value() << std::endl;
    }
 
    return 0;
}

Xử Lý Nil trong Go

package main
 
import "fmt"
 
func processPointer(p *int) {
    // Phải kiểm tra nil thủ công
    if p != nil {
        fmt.Println("Giá trị:", *p)
    } else {
        fmt.Println("Nil pointer!")
    }
}
 
// Go idiomatic: trả về nhiều giá trị
func findValue(id int) (int, bool) {
    if id > 0 {
        return id * 10, true
    }
    return 0, false
}
 
// Hoặc trả về pointer (nil = không tìm thấy)
func findValuePtr(id int) *int {
    if id > 0 {
        result := id * 10
        return &result
    }
    return nil
}
 
func main() {
    var null *int  // nil mặc định
 
    // Dereference nil = panic!
    // fmt.Println(*null)  // panic: runtime error
 
    processPointer(null)
 
    // Sử dụng multiple return values
    if value, ok := findValue(5); ok {
        fmt.Println("Tìm thấy:", value)
    }
 
    // Sử dụng pointer
    if ptr := findValuePtr(5); ptr != nil {
        fmt.Println("Tìm thấy:", *ptr)
    }
}

Khác Biệt Chính:

  • C++: Dereference null là undefined behavior (bất cứ điều gì có thể xảy ra)
  • Go: Dereference nil gây ra panic (crash có kiểm soát với stack trace)

References: Tính Năng C++, Không Có Trong Go

C++ có references, giống như pointer tự động dereference. Go không có tính năng này.

References trong C++

#include <iostream>
 
void modifyByReference(int& ref) {
    ref = 100;  // Không cần *, sửa đổi bản gốc
}
 
void modifyByPointer(int* ptr) {
    *ptr = 200;  // Phải dùng * để dereference
}
 
int main() {
    int x = 42;
 
    // Reference: phải khởi tạo, không thể null
    int& ref = x;
    ref = 50;
    std::cout << "x sau ref: " << x << std::endl;  // 50
 
    // Reference như tham số hàm
    modifyByReference(x);
    std::cout << "x sau modifyByReference: " << x << std::endl;  // 100
 
    // Pointer như tham số hàm
    modifyByPointer(&x);
    std::cout << "x sau modifyByPointer: " << x << std::endl;  // 200
 
    // const reference: có thể bind với rvalue
    const int& constRef = 42;  // Hoạt động!
    // int& badRef = 42;       // Lỗi: không thể bind với rvalue
 
    return 0;
}

Go: Chỉ Có Pointers

package main
 
import "fmt"
 
// Go dùng pointers cho pass-by-reference semantics
func modifyByPointer(ptr *int) {
    *ptr = 100  // Phải dùng * để dereference
}
 
// Không có cú pháp reference - cái này copy
func modifyByCopy(val int) {
    val = 200  // Chỉ sửa đổi bản copy
}
 
func main() {
    x := 42
 
    // Go không có references, chỉ có pointers
    ptr := &x
    *ptr = 50
    fmt.Println("x sau pointer:", x)  // 50
 
    // Pointer như tham số hàm
    modifyByPointer(&x)
    fmt.Println("x sau modifyByPointer:", x)  // 100
 
    // Giá trị được copy, bản gốc không đổi
    modifyByCopy(x)
    fmt.Println("x sau modifyByCopy:", x)  // Vẫn 100
}

Tại Sao Go Không Có References:

  1. Rõ ràng: Pointers làm rõ khi nào mutation xảy ra
  2. Đơn giản: Một cách để làm việc, không phải hai
  3. Nhất quán: Luôn dùng * để dereference

Smart Pointers (C++) vs Cách Tiếp Cận của Go

C++ giới thiệu smart pointers để xử lý bộ nhớ tự động. Garbage collector của Go làm cho những thứ này không cần thiết.

Smart Pointers trong C++

#include <iostream>
#include <memory>
 
class Resource {
public:
    std::string name;
    Resource(std::string n) : name(n) {
        std::cout << "Đã tạo: " << name << std::endl;
    }
    ~Resource() {
        std::cout << "Đã hủy: " << name << std::endl;
    }
};
 
int main() {
    // unique_ptr: sở hữu độc quyền
    {
        std::unique_ptr<Resource> unique =
            std::make_unique<Resource>("unique");
        // Tự động xóa khi kết thúc scope
    }  // "Đã hủy: unique" in ở đây
 
    // shared_ptr: sở hữu chung với reference counting
    std::shared_ptr<Resource> shared1 =
        std::make_shared<Resource>("shared");
    {
        std::shared_ptr<Resource> shared2 = shared1;
        std::cout << "Count: " << shared1.use_count() << std::endl;  // 2
    }
    std::cout << "Count: " << shared1.use_count() << std::endl;  // 1
    // Xóa khi shared_ptr cuối cùng hết scope
 
    // weak_ptr: reference không sở hữu
    std::weak_ptr<Resource> weak = shared1;
    if (auto locked = weak.lock()) {
        std::cout << "Vẫn còn: " << locked->name << std::endl;
    }
 
    return 0;
}

Go: Garbage Collection Xử Lý Mọi Thứ

package main
 
import "fmt"
 
type Resource struct {
    name string
}
 
func main() {
    // Go không có smart pointers - GC xử lý bộ nhớ
 
    // Tương đương unique_ptr: chỉ dùng pointer
    {
        unique := &Resource{name: "unique"}
        fmt.Println("Đang dùng:", unique.name)
    }
    // GC sẽ dọn dẹp khi không còn references
 
    // Tương đương shared_ptr: tất cả pointers đều "shared"
    shared1 := &Resource{name: "shared"}
    shared2 := shared1  // Cả hai trỏ đến cùng object
    fmt.Println("shared1:", shared1.name)
    fmt.Println("shared2:", shared2.name)
    // GC theo dõi tất cả references tự động
 
    // Không có tương đương weak_ptr - Go theo dõi tất cả references
    // Nếu cần weak references, dùng map với explicit cleanup
}

So Sánh:

C++ Smart PointerTương Đương Go
std::unique_ptrPointer thường (GC xử lý cleanup)
std::shared_ptrPointer thường (GC làm ref counting)
std::weak_ptrKhông có tương đương trực tiếp; dùng maps/callbacks
Custom deletersruntime.SetFinalizer (không khuyến khích)

Struct Pointers

Cả hai ngôn ngữ đều dùng pointers đến structs, nhưng với cú pháp khác nhau.

Struct/Class Pointers trong C++

#include <iostream>
 
struct Person {
    std::string name;
    int age;
};
 
int main() {
    Person person{"Alice", 30};
    Person* ptr = &person;
 
    // Hai cách truy cập members qua pointer:
    std::cout << (*ptr).name << std::endl;  // Dereference rồi truy cập
    std::cout << ptr->name << std::endl;    // Arrow operator (ưa thích)
 
    // Sửa đổi qua pointer
    ptr->age = 31;
    std::cout << person.age << std::endl;  // 31
 
    // Cấp phát heap
    Person* heapPerson = new Person{"Bob", 25};
    std::cout << heapPerson->name << std::endl;
    delete heapPerson;
 
    return 0;
}

Struct Pointers trong Go

package main
 
import "fmt"
 
type Person struct {
    Name string
    Age  int
}
 
func main() {
    person := Person{Name: "Alice", Age: 30}
    ptr := &person
 
    // Go tự động dereference cho struct access
    fmt.Println(ptr.Name)    // Không cần ->! Tự động dereference
    fmt.Println((*ptr).Name) // Cũng hoạt động, nhưng không idiomatic
 
    // Sửa đổi qua pointer
    ptr.Age = 31
    fmt.Println(person.Age)  // 31
 
    // Tạo pointer trực tiếp
    heapPerson := &Person{Name: "Bob", Age: 25}
    fmt.Println(heapPerson.Name)
    // Không cần delete - GC xử lý
}

Khác Biệt Chính:

  • C++: Dùng -> cho pointer member access, . cho value member access
  • Go: Luôn dùng . - Go tự động dereference cho bạn

Truyền Pointer trong Hàm

Tham Số Hàm trong C++

#include <iostream>
#include <vector>
 
// Truyền theo giá trị (copy)
void passByValue(std::vector<int> vec) {
    vec.push_back(100);  // Chỉ sửa đổi bản copy
}
 
// Truyền theo pointer
void passByPointer(std::vector<int>* vec) {
    vec->push_back(100);  // Sửa đổi bản gốc
}
 
// Truyền theo reference (chỉ C++)
void passByReference(std::vector<int>& vec) {
    vec.push_back(100);  // Sửa đổi bản gốc, cú pháp gọn hơn
}
 
// Truyền theo const reference (tốt nhất cho read-only)
void passByConstRef(const std::vector<int>& vec) {
    // vec.push_back(100);  // Lỗi: không thể sửa đổi
    std::cout << "Size: " << vec.size() << std::endl;
}
 
int main() {
    std::vector<int> vec = {1, 2, 3};
 
    passByValue(vec);
    std::cout << "Sau value: " << vec.size() << std::endl;  // 3
 
    passByPointer(&vec);
    std::cout << "Sau pointer: " << vec.size() << std::endl;  // 4
 
    passByReference(vec);
    std::cout << "Sau reference: " << vec.size() << std::endl;  // 5
 
    passByConstRef(vec);
 
    return 0;
}

Tham Số Hàm trong Go

package main
 
import "fmt"
 
// Truyền theo giá trị (copy slice header, không phải underlying array)
func passByValue(slice []int) {
    slice = append(slice, 100)  // Có thể hoặc không ảnh hưởng bản gốc
    // Lưu ý: slices có điều khó - header được copy, không phải array
}
 
// Truyền theo pointer (sửa đổi rõ ràng)
func passByPointer(slice *[]int) {
    *slice = append(*slice, 100)  // Chắc chắn sửa đổi bản gốc
}
 
// Với structs - truyền theo giá trị (copy)
func updatePersonValue(p Person) {
    p.Age = 100  // Chỉ sửa đổi bản copy
}
 
// Với structs - truyền theo pointer (sửa đổi bản gốc)
func updatePersonPointer(p *Person) {
    p.Age = 100  // Sửa đổi bản gốc
}
 
type Person struct {
    Name string
    Age  int
}
 
func main() {
    // Slices giống reference nhưng có những điểm tinh tế
    slice := []int{1, 2, 3}
    passByValue(slice)
    fmt.Println("Sau value:", len(slice))  // 3 (không đổi)
 
    passByPointer(&slice)
    fmt.Println("Sau pointer:", len(slice))  // 4
 
    // Structs
    person := Person{Name: "Alice", Age: 30}
 
    updatePersonValue(person)
    fmt.Println("Sau value:", person.Age)  // 30 (không đổi)
 
    updatePersonPointer(&person)
    fmt.Println("Sau pointer:", person.Age)  // 100
}

So Sánh Best Practices

Best Practices trong C++

// 1. Ưu tiên smart pointers hơn raw pointers
std::unique_ptr<Resource> ptr = std::make_unique<Resource>();
 
// 2. Dùng references cho tham số hàm khi có thể
void process(const std::vector<int>& data);
 
// 3. Dùng nullptr, không phải NULL hoặc 0
int* ptr = nullptr;
 
// 4. Kiểm tra pointers trước khi dereference
if (ptr != nullptr) {
    process(*ptr);
}
 
// 5. Tuân theo RAII - resources nên được sở hữu bởi objects
class FileHandler {
    std::unique_ptr<FILE, decltype(&fclose)> file;
public:
    FileHandler(const char* path) : file(fopen(path, "r"), &fclose) {}
};
 
// 6. Tránh pointer arithmetic khi có thể
// Dùng iterators và ranges thay vì
for (const auto& item : container) {
    process(item);
}

Best Practices trong Go

// 1. Dùng pointers khi cần sửa đổi
func (p *Person) SetAge(age int) {
    p.Age = age
}
 
// 2. Dùng values cho structs nhỏ, immutable
func (p Point) Distance() float64 {
    return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}
 
// 3. Kiểm tra nil trước khi dereference
if ptr != nil {
    process(*ptr)
}
 
// 4. Đừng trả về pointer đến biến local chỉ để tránh copy
// (trừ khi struct lớn)
// OK: func getPoint() Point { return Point{X: 1, Y: 2} }
// Chỉ nếu lớn: func getLargeStruct() *LargeStruct { ... }
 
// 5. Dùng slices và maps (reference types) tự nhiên
func process(data []int) {  // Không cần pointer
    // Sửa đổi data[i] ảnh hưởng caller
}
 
// 6. Nhất quán với receiver types
type Counter struct{ count int }
func (c *Counter) Increment() { c.count++ }  // Tất cả methods dùng pointer
func (c *Counter) Value() int { return c.count }

Tóm Tắt: Khi Nào Dùng Gì

Dùng Pointers Trong Cả Hai Ngôn Ngữ Khi:

Tình huốngGoC++
Sửa đổi giá trị gốc*TypeType* hoặc Type&
Struct lớn (tránh copy)*Typeconst Type& hoặc Type*
Giá trị optional/nullable*Type (nil)Type* hoặc std::optional
PolymorphismInterface + pointerBase* hoặc Base&

Tóm Tắt Khác Biệt Chính

Tính năngGoC++
Pointer arithmetic❌ Không cho phép✅ Hỗ trợ đầy đủ
Quản lý bộ nhớGarbage collectedThủ công (hoặc smart pointers)
References❌ Chỉ có pointers✅ Cú pháp &
Giá trị nullnilnullptr
Struct accessLuôn .. cho values, -> cho pointers
Smart pointersKhông cầnunique_ptr, shared_ptr, v.v.
Escape analysis✅ Tự động❌ Quyết định heap/stack thủ công

Bài Viết Liên Quan


Kết Luận

Mặc dù cả Go và C++ đều sử dụng pointers, nhưng chúng đại diện cho các triết lý cơ bản khác nhau:

C++ cho bạn kiểm soát hoàn toàn bộ nhớ, bao gồm pointer arithmetic, quản lý bộ nhớ thủ công và tối ưu hóa chi tiết. Sức mạnh này đi kèm với trách nhiệm - bạn phải xử lý bộ nhớ đúng cách hoặc đối mặt với bugs.

Go đánh đổi một số kiểm soát để có được an toàn. Bằng cách loại bỏ pointer arithmetic và thêm garbage collection, Go loại bỏ toàn bộ các category bugs trong khi vẫn đủ hiệu quả cho hầu hết các ứng dụng.

Không cách tiếp cận nào là "tốt hơn" một cách tuyệt đối - hãy chọn dựa trên yêu cầu của bạn:

  • Chọn C++ khi bạn cần hiệu suất tối đa, kiểm soát hardware, hoặc làm việc trong môi trường hạn chế (embedded, games, OS kernels)
  • Chọn Go khi bạn muốn phát triển năng suất, an toàn và hiệu suất "đủ tốt" (servers, CLI tools, cloud applications)

Hiểu cả hai cách tiếp cận giúp bạn trở thành lập trình viên tốt hơn trong cả hai ngôn ngữ!

📬 Đăng ký nhận bản tin

Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.

💬 Comments

Đăng nhập để bình luận

Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.