四大系统级编程语言对比(语言层面)

· · 科技·工程

在我的个人博客算法星河获得更好的阅读体验。此文章为多平台转载,勘误纠正等更新首先在博客内更新,再转移至此平台文章。

本文进行四大系统级编程语言在语言层面的对比。发现本文勘误、不合理之处,可以进行指出。目前博客暂未启动评论区。

本文进行对比时,均采用当时最新版本的语言。

本文的大致框架:

  1. 语言特性的对比。这部分包括语言的类型系统、内存管理、编程范式的对比。
  2. 语言语法的对比。这部分包括语言的类型名称、类型声明方式、各种语句和语句块的写法的对比。
  3. 程序结构化对比。这部分包括在语言的函数定义、结构(类)定义、枚举定义的写法的对比。
  4. 数据共享的对比。这部分包括语言如何进行数据的共享(不复制)。
  5. 程序设计的对比。这部分位于文章结尾,使用四种语言编写了一个最小生成树的算法,均使用工程代码规范。如果你想要快速地、全局地了解语言差异,优先阅读此部分。

四大系统级编程语言

本文所指的四大系统级编程语言分别是:C、C++、Go、Rust。这是目前公认的四种,即便 Go 语言存在一定争议。由于 Go 语言在性能上可以追上 C 与 C++,而语法上又大部分借鉴于 C 语言,使用方法也类似,也不止可以用在高并发网络领域,所以我们把 Go 也算作其中一员。

关于本文的定义

为了更好地对编程语言进行分类,本文中使用了一些基于又高于学界公认的分类标准和定义。我们会给出本文使用的分类标准的定义。

语言特性对比

类型系统

定义:

语言 类型动态性 类型严格性 共享数据类型
C 静态类型 强类型 基于指针
C++ 静态类型 强类型 基于指针
Go 静态类型 严格类型 基于引用
Rust 静态类型 严格类型 基于借用(所有权)

:::info[提示]{open} Go 语言中有指针类型,但其指针类型行为与 Java 的引用类似,使用垃圾回收机制,所以归为基于引用的类型系统。 :::

内存管理

定义:

对于智能指针管理和垃圾回收自动内存管理的区别,前者不需要运行时或者后台暂停,而后者需要。

语言 内存管理形式
C 手动内存管理
C++ 手动内存管理
Go 垃圾回收自动内存管理
Rust 生命周期自动内存管理

:::info[提示]{open} C++ 有 std::shared_ptrstd::unique_ptr 等类型,它们是智能指针。但是它们是基于标准库实现的包装类型,只是将手动内存管理的任务包装起来了而已。所以在语言层面我们不认为 C++ 有智能指针管理。

Rust 语言中有 RcArc 类型,它们也是智能指针。但它没有打破 Rust 的“一个数据只能有一个所有者”的限制,它仅仅包装了一个共享的数据,对于内部的这个数据来说,类似于多所有者。Rust 将 RcArc 类型视作普通类型,一视同仁地进行生命周期分析,并决定分配和释放的位置,以及检查借用合法性。所以此处不认为 Rust 语言层面有智能指针管理。 :::

编程范式

定义:

语言 编程范式(第一位加粗为主要范式,后无先后顺序)
C 过程式、命令式
C++ 面向对象、行为抽象(抽象类与纯抽象类)、过程式、函数式(标准库提供)、元编程(模板元编程和标准库提供的有限的运行时多类型)、泛型编程(模板元编程)、命令式
Go 行为抽象(使用接口 interface)、过程式、函数式(部分)、元编程(运行时反射,使用 reflect 包)、泛型编程(后期引入类型参数)、命令式
Rust 行为抽象(使用特征 trait)、过程式、函数式(极其强大的迭代器)、元编程(编译时抽象语法树『AST』的修改,使用过程宏和属性宏)、泛型编程、命令式

语言语法对比

数据类型对比

类型 C C++ Go Rust
平台特定整数 intunsigned int < intuint usizeisize
8 位整数 int8_tuint8_t std::int8_tstd::uint8_t int8uint8 i8u8
16 位整数 int16_tuint16_t std::int16_tstd::uint16_t int16uint16 i16u16
32 位整数 int32_tuint32_t std::int32_tstd::uint32_t int32uint32 i32u32
64 位整数 int64_tuint64_t std::int64_tstd::uint64_t int64uint64 i64u64
128 位整数 标准内无 < < i128u128
单精度浮点型 float < float32 f32
双精度浮点型 double < float64 f64
长双精度浮点型(硬件依赖) long double < 标准内无 <
布尔型 bool < < <
单精度复数 float _Complex std::complex<float> complex64 std::complex::Complex<f32>
双精度复数 double _Complex std::complex<double> complex128 std::complex::Complex<f64>
高精度整数 标准内无,需手动实现或第三方库 < 内置 big.Int 第三方库 num_bigint::BigInt
高精度浮点数 标准内无,需手动实现或第三方库 < 内置 big.Float 第三方库 decimal::d128
分数 / 有理数 标准内无,需手动实现 第三方库 boost::rational<int_type> 标准库扩展 big.Rat 第三方库 num_rational::Ratio
空类型 / 无返回类型 void < 无显式,可用空结构 struct{} ()!
共享数据类型(所有) 指针 type* 指针 type* 与引用 type& 类指针 *type 借用 &type&mut type
字符类型 char 单字节 / UChar32 ICU 第三方库提供的 Unicode 字符 char 遗留单字节 / wchar_t 不推荐使用的宽字符 / char8_t UTF-8 / char16_t UTF-16 / char32_t UTF-32 或 Unicode 字符 byte 单字节 / rune Unicode 字符 u8 单字节 / char Unicode 字符

静态数组对比

仅展示类型声明形式,不考虑变量等问题。

C

int array[100];

C++

int origin[100];  // 原始
std::array<int, 100> modern;  // 现代

Go

var array [100]int

Rust

let array = [i32; 100];

动态数组对比

仅展示类型声明形式,不考虑变量等问题。

C

手动实现。使用指针管理内存:

int* array;

动态分配和释放内存:

int* array = malloc(3 * sizeof(int));  // 分配
array[0] = 1;
array[1] = 2;
array[2] = 3;
free(array);  // 释放

C++

std::vector<int> array{ 1, 2, 3 };  // 初始化
std::vector<int> arr_with_len(10);  // 带长度

Go

array := []int{ 1, 2, 3 }  // 初始化
arrWithLen := make([]int, 10)  // 带长度

Rust

// 加了显示类型声明
let array: Vec<i32> = vec![1, 2, 3];  // 初始化
let arr_with_len: Vec<_> = vec![0; 10];  // 带长度,自动类型推导

字符串对比

C

const char* str1 = "Hello!";  // 字面量
char* str2 = malloc(7 * sizeof(char));  // 动态
//  依靠第三方库 ICU 可以(很复杂地)实现 Unicode 字符串

C++

const char* str1 = "Hello!";  // 字面量
std::string str2;  // 现代,可表示 UTF-8
std::u16string str3;  // UTF-16
std::u32string str4;  // UTF-32
icu::UnicodeString str5;  // 第三方库 ICU

Go

str1 := "Hello!"  // 字面量
var str2 string  // 显式声明类型,UTF-8

Rust

let str1: &str = "Hello!";  // 字面量
let str2: String;  // 动态,强制 UTF-8

哈希表对比

C

手动实现。

C++

std::unordered_map<std::string, int> table = {
    { "Perfect": 100 },
    { "Excellent": 80},
    { "Pass": 60 },
    { "Fail": 0 },
};

Go

table := map[string]int{
    "Perfect": 100,
    "Excellent": 80,
    "Pass": 60,
    "Fail": 0,
}

Rust

let mut table = HashMap::new();
table.insert("Perfect", 100);
table.insert("Excellent", 80);
table.insert("Pass", 60);
table.insert("Fail", 0);

控制流对比

条件判断

C

if (cond) {
    // ...
}

if (cond) {
    // ...
} else {
    // ...
}

if (cond1) {
    // ...
} else if (cond2) {
    // ...
} else if (cond3) {
    // ...
} else {
    // ...
}

C++

if (cond) {
    // ...
}

if (cond) {
    // ...
} else {
    // ...
}

if (cond1) {
    // ...
} else if (cond2) {
    // ...
} else if (cond3) {
    // ...
} else {
    // ...
}

if (int num = 1; cond) {  // 带初始临时变量
    // ...
} // else-if else

Go

if cond {
    // ...
}

if cond {
    // ...
} else {
    // ...
}

if cond1 {
    // ...
} else if cond2 {
    // ...
} else if cond3 {
    // ...
} else {
    // ...
}

Rust

if cond {
    // ...
}

if cond {
    // ...
} else {
    // ...
}

if cond1 {
    // ...
} else if cond2 {
    // ...
} else if cond3 {
    // ...
} else {
    // ...
}

if let Pattern = something {  // 模式匹配判断
    // ...
} // else-if else

迭代循环

C

for (int i = 0; i < 100; i++) {
    // ...
}

C++

for (int i = 0; i < 100; i++) {
    // ...
}

for (int i : std::views::iota(0, 100)) {
    // ...
}

Go

for i := 0; i < 100; i++ {
    // ...
}

for i := range [100]struct{} {
    // ...
}

Rust

for i in 0..100 {
    // ...
}

范围循环

C

无。

C++

for (auto ele : something) {
    // ...
}

Go

for _, ele := range something {
    // ...
}

Rust

for ele in &something {
    // ...
}

for ele in something.iter() {
    // ...
}

条件循环

C

while (cond) {
    // ...
}

for (;cond;) {
    // ...
}

C++

while (cond) {
    // ...
}

for (;cond;) {
    // ...
}

Go

for cond {
    // ...
}

for ;cond; {
    // ...
}

Rust

while cond {
    // ...
}

while let Pattern = something {  // 模式匹配循环
    // ...
}

无限循环

C

while (true) {
    // ...
}

for (;true;) {
    // ...
}

for (;;) {
    // ...
}

C++

while (true) {
    // ...
}

for (;true;) {
    // ...
}

for (;;) {
    // ...
}

Go

for {
    // ...
}

for true {
    // ...
}

for ;true; {
    // ...
}

for ;; {
    // ...
}

Rust

loop {
    // ...
}

while true {
    // ...
}

匹配选择

C

switch (value) {
    case val1:
        // ...
        break;
    case val2:
        // ...
        // fallthrough
    case val3:
        // ...
        break;
    default:
        // ...
        break;
}

C++

switch (value) {
    case val1:
        // ...
        break;
    case val2:
        // ...
        // fallthrough
    case val3:
        // ...
        break;
    default:
        // ...
        break;
}

Go

switch value {
case val1:
    // ...
case val2:
    // ...
    fallthrough
case val3:
    // ...
default:
    // ...
}

switch {
case cond1:  // 布尔条件
    // ...
default:
    // ...
}

Rust

match value {  // 模式匹配
    Pattern1 => /* ... */,
    Pattern2 => {
        // ...
    }
    Pattern3 => {
        // ...
    }
    _ => {
        // ...
    }
}

程序结构化对比

函数

C

// 无参无返回
void no_arg_no_ret() {
    // ...
}
// 旧式
void no_arg_no_ret_old(void) {
    // ...
}

// 无参有返回
int no_arg_has_ret() {
    return 1;
}
int no_arg_has_ret_old(void) {
    return 1;
}

// 有参无返回
void has_arg_no_ret(int a, double b) {
    // ...
}

// 有参有返回
int has_arg_has_ret(int a, int b) {
    return a + b;
}

C++

// 无参无返回
void no_arg_no_ret() {
    // ...
}
auto no_arg_no_ret_new() -> void {
    // ...
}

// 无参有返回
int no_arg_has_ret() {
    return 1;
}
auto no_arg_has_ret_auto() {
    return 1;
}
auto no_arg_has_ret_new() -> int {
    return 1;
}

// 有参无返回
void has_arg_no_ret(int a, double b) {
    // ...
}
auto has_arg_no_ret_new(int a, double b) -> void {
    // ...
}

// 有参有返回
int has_arg_has_ret(int a, int b) {
    return a + b;
}
auto has_arg_has_ret_auto(int a, int b) {
    return a + b;
}
auto has_arg_has_ret_decl(int a, int b) -> decltype(a + b) {
    return a + b;
}
auto has_arg_has_ret_new(int a, int b) -> int {
    return a + b;
}

Go

// 无参无返回
func NoArgNoRet() {
    // ...
}

// 无参有返回
func NoArgHasRet() int {
    return 1
}
func NoArgHasRetWithName() ret int {
    ret := 1
    return
}

// 有参无返回
func HasArgNoRet(a int, b float64) {
    // ...
}

// 有参有返回
func HasArgHasRet(a, b int) int {
    return a + b
}
func HasArgHasRetWithName(a, b int) sum int {
    sum := a + b
    return
}
func HasArgHasRetExplicitTypes(a int, b int) int {
    return a + b
}

Rust

// 无参无返回
fn no_arg_no_ret() {
    // ...
}

// 无参有返回
fn no_arg_has_ret() -> i32 {
    1
}
fn no_arg_has_ret_with_return() -> i32 {
    return 1;
}

// 有参无返回
fn has_arg_no_ret(a: i32, b: f64) {
    // ...
}

// 有参有返回
fn has_arg_has_ret(a: i32, b: i32) -> i32 {
    a + b
}
fn has_arg_has_ret_with_return(a: i32, b: i32) -> i32 {
    return a + b;
}

结构(类)定义

C

struct Type {
    int a;
    double b;
    char c;
    const char* name;
};

C++

struct Type {
    // 默认 public
    int a;
public:
    double b;
protected:
    char c;
private:
    std::string name;

public:
    void method() {
        // ...
    }

    static void static_method() {
        // ...
    }
};

class TypeClass {
    // 默认 private
    int a;
public:
    double b;
protected:
    char c;
private:
    std::string name;

public:
    void method() {
        // ...
    }

    static void static_method() {
        // ...
    }
};

Go

type Type struct {
    a int
    b float64
    c rune
    Name string  // public
}

func (this *Type) Method() {
    // ...
}

Rust

struct Type {
    a: i32;
    b: f64;
    c: char;
    pub name: String;  // public
}

impl Type {
    pub fn method(&self) {
        // ...
    }

    pub fn static_method() {
        // ...
    }
}

枚举定义

C

enum Day {
    Sun,
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,
};

C++

enum Day {
    Sun,
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,
};

enum class DayEnum {
    Sun,
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat,
};

Go

const (
    Sun = iota
    Mon
    Tue
    Wed
    Thu
    Fri
    Sat
)

Rust

enum Day {
    Sun,
    Mon,
    Tue,
    Wed,
    Thu,
    Fri,
    Sat
}

// 枚举变体
enum Message {
    Shutdown,
    Send(String),
    Error {
        code: usize,
        msg: String,
    },
    Sleep(usize, usize),
}

数据共享对比

C

指针

int number = 1;
int* pointer = &number;  // 获取指针
int other = *pointer;  // 获取指针值
*pointer = 2;  // 设置指针值
pointer = &other;  // 重设目标

指向不可变指针(常量指针)

int number = 1;
int* const pointer = &number;
int other = *pointer;
*pointer = 2;
// pointer = &other;  --- Error

数据不可变指针(指向常量的指针)

int number = 1;
const int* pointer = &number;
int other = *pointer;
// *pointer = 2;  --- Error
pointer = &other;

指向与数据不可变指针(指向常量的常量指针)

int number = 1;
const int* const pointer = &number;
int other = *pointer;
// *pointer = 2;  --- Error
// pointer = &other;  --- Error

C++

指针

与 C 语言完全一致。

引用

int number = 1;
int& reference = number;  // 创建引用
int other = reference;  // 读取引用值
reference = 2;  // 设置引用值

不可变引用

int number = 1;
const int& reference = number;
int other = reference;
// reference = 2;  --- Error

Go

指针

显式定义类型:

var number int = 1
var pointer *int = &number  // 创建指针
var other int = *pointer  // 读取指针值
*pointer = 2  // 设置指针值
pointer = &other  // 重设目标

简写声明:

number := 1
pointer := &number
other := *pointer
*pointer = 2
pointer = &other

Rust

可变借用

let mut number = 1;
let borrow = &mut number;  // 创建借用
let mut other = *borrow;  // 读取借用值(若实现 Copy 则复制,否则转移所有权)
*borrow = 2;  // 设置借用值(若实现 Copy 则复制,否则转移所有权)
// borrow = &mut other;  --- 需要 mut 变量才能重设目标

不可变借用

let number = 1;
let borrow = &number;
let other = *borrow;
// *borrow = 2;  --- Error

程序设计对比

接下来,我们用这四种语言实现同一个程序,来从全局的角度对比一下这四种语言。

程序目标

输入一个无向带权图,该图不一定连通。求出此图的最小生成树的边权之和,如果此图不连通,则输出 Not connected.

从标准输入读取图的信息。第一行两个整 VE,表示图的点数与边数。接下来共 M 行,每行参个整数 abw,代表点 a 与点 b 之间有一条无向边,边权是 w

将结果输出到标准输出。如果该图连通,输出最小生成树的边权之和;否则,输出 Not connected.

点的序号 ab 采用以 1-based 索引(即索引从 1 开始)。

程序设计

我们使用 Kruskal 算法求最小生成树。简单说明一下算法思路:输入所有边后,对边按照边权升序排序,并从最小边权的边开始处理。使用并查集来处理哪些顶点在已知的最小生成树中,哪些不在。每次处理一条边时,需要判断这个边所连接的两个点是否已经都在最小生成树中了,也就是这两个点是否已经在并查集中连通了。只要有一个点不在已知的最小生成树中,我们将此边加入最小生成树,在并查集中连通两个顶点,并累加边权。这是一种典型的贪心算法,时间复杂度为 O\left(E \log E\right),空间复杂度为 O\left(E + V\right),其中 E 是边数,V 是点数,计算机科学中对数的默认底为 2

根据树的性质:边段数量为点的数量减一。所以当我们处理了足够多的边时,停止处理并输出累加边权。

如果最后所有的边处理完成但是已知的最小生成树中还没有足够的边,说明原图不连通。这时我们输出 Not connected.

标准输入的节点是 1-based 索引,为了方便处理,我们在进行数据输入时就将其转换为 0-based 索引。

关于图、树、最小生成树、Kruskal 算法、贪心算法等内容,感兴趣的可以自行搜索更多资料。

代码

C

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>

typedef int32_t int32;
typedef size_t usize;

typedef struct {
    int32* father;
} UnionFind;

typedef struct {
    int32 a;
    int32 b;
    int32 weight;
} Edge;

int edge_cmp(const void* a, const void* b);
void read_data(usize* node_cnt, usize* edge_cnt, Edge** const edges);
bool kruskal(const usize node_cnt, const usize edge_cnt, Edge* const edges, int32* const result);
UnionFind* create_union_find(const usize size);
void free_union_find(UnionFind* const self);
int32 find_root(UnionFind* const self, const int32 a);
bool connected(UnionFind* const self, const int32 a, const int32 b);
void merge(UnionFind* const self, const int32 a, const int32 b);

int main() {
    usize node_cnt = 0;
    usize edge_cnt = 0;
    Edge* edges = NULL;

    read_data(&node_cnt, &edge_cnt, &edges);

    int32 result = -1;
    const bool succeeded = kruskal(node_cnt, edge_cnt, edges, &result);

    if (succeeded) {
        printf("%" PRId32 "\n", result);
    } else {
        printf("Not connected.\n");
    }

    free(edges);

    return 0;
}

int edge_cmp(const void* a, const void* b) {
    const Edge* edge_a = (const Edge*) a;
    const Edge* edge_b = (const Edge*) b;
    if (edge_a->weight < edge_b->weight) {
        return -1;
    } else if (edge_a->weight > edge_b->weight) {
        return 1;
    } else {
        return 0;
    }
}

void read_data(usize* node_cnt, usize* edge_cnt, Edge** const edges) {
    const int input_res = scanf("%zu%zu", node_cnt, edge_cnt);
    if (input_res != 2) {
        exit(1);
    }

    *edges = (Edge*) malloc(*edge_cnt * sizeof(Edge));
    if (*edges == NULL) {
        exit(1);
    }

    for (usize i = 0; i < *edge_cnt; i++) {
        int32 a, b, weight;
        const int input_res = scanf("%" PRId32 "%" PRId32 "%" PRId32, &a, &b, &weight);
        if (input_res != 3) {
            exit(1);
        }
        (*edges)[i] = (Edge){
            .a = a - 1,
            .b = b - 1,
            .weight = weight,
        };
    }
}

bool kruskal(const usize node_cnt, const usize edge_cnt, Edge* const edges, int32* const result) {
    qsort(edges, edge_cnt, sizeof(Edge), edge_cmp);

    UnionFind* const set = create_union_find(node_cnt);

    int32 total = 0;
    int32 handled = 0;
    for (usize i = 0; i < edge_cnt; i++) {
        const Edge* const edge = &edges[i];
        if (connected(set, edge->a, edge->b)) {
            continue;
        }
        merge(set, edge->a, edge->b);
        total += edge->weight;
        handled++;
        if (handled == (int32) node_cnt - 1) {
            *result = total;
            free_union_find(set);
            return true;
        }
    }

    free_union_find(set);
    return false;
}

UnionFind* create_union_find(const usize size) {
    UnionFind* const self = (UnionFind* const) malloc(sizeof(UnionFind));
    if (self == NULL) {
        exit(1);
    }

    self->father = (int32*) malloc(size * sizeof(int32));
    if (self->father == NULL) {
        exit(1);
    }

    for (usize i = 0; i < size; i++) {
        self->father[i] = (int32) i;
    }

    return self;
}

void free_union_find(UnionFind* const self) {
    free(self->father);
    free(self);
}

int32 find_root(UnionFind* const self, const int32 a) {
    if (self->father[a] == a) {
        return a;
    }
    const int32 temp = find_root(self, self->father[a]);
    self->father[a] = temp;
    return temp;
}

bool connected(UnionFind* const self, const int32 a, const int32 b) {
    return find_root(self, a) == find_root(self, b);
}

void merge(UnionFind* const self, const int32 a, const int32 b) {
    const int32 root_a = find_root(self, a);
    const int32 root_b = find_root(self, b);
    self->father[root_a] = root_b;
}

C++

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <memory>
#include <optional>
#include <tuple>
#include <vector>

using int32 = std::int32_t;
using usize = std::size_t;

class UnionFind final {
private:
    const std::unique_ptr<int32[]> father;
    auto find_root(const int32 a) -> int32;

public:
    UnionFind(const usize size);
    auto connected(const int32 a, const int32 b) -> bool;
    void merge(const int32 a, const int32 b);
};

class Edge final {
public:
    int32 a;
    int32 b;
    int32 weight;

    Edge(const int32 a, const int32 b, const int32 weight);
};

auto read_data() -> std::tuple<usize, std::vector<Edge>>;
auto kruskal(const usize node_cnt, std::vector<Edge>& edges) -> std::optional<int32>;

auto main() -> int {
    auto [node_cnt, edges] = read_data();

    const std::optional<int32> result = kruskal(node_cnt, edges);

    if (result.has_value()) {
        std::cout << result.value() << '\n';
    } else {
        std::cout << "Not connected." << '\n';
    }

    return 0;
}

auto read_data() -> std::tuple<usize, std::vector<Edge>> {
    usize node_cnt, edge_cnt;
    std::cin >> node_cnt >> edge_cnt;

    std::vector<Edge> edges;
    for (usize i = 0; i < edge_cnt; i++) {
        int32 a, b, weight;
        std::cin >> a >> b >> weight;
        edges.push_back(Edge(a - 1, b - 1, weight));
    }

    return std::make_tuple(node_cnt, std::move(edges));
}

auto kruskal(const usize node_cnt, std::vector<Edge>& edges) -> std::optional<int32> {
    std::ranges::sort(edges, [](const Edge& a, const Edge& b) -> bool {
        return a.weight < b.weight;
    });

    UnionFind set(node_cnt);

    int32 total = 0;
    int32 handled = 0;
    for (const Edge& edge : edges) {
        if (set.connected(edge.a, edge.b)) {
            continue;
        }
        set.merge(edge.a, edge.b);
        total += edge.weight;
        handled++;
        if (handled == static_cast<int32>(node_cnt) - 1) {
            return std::make_optional(total);
        }
    }

    return std::nullopt;
}

UnionFind::UnionFind(const usize size) : father(std::make_unique<int32[]>(size)) {
    for (usize i = 0; i < size; i++) {
        father[i] = static_cast<int32>(i);
    }
}

auto UnionFind::connected(const int32 a, const int32 b) -> bool {
    return find_root(a) == find_root(b);
}

void UnionFind::merge(const int32 a, const int32 b) {
    const int32 root_a = find_root(a);
    const int32 root_b = find_root(b);
    father[root_a] = root_b;
}

auto UnionFind::find_root(const int32 a) -> int32 {
    if (father[a] == a) {
        return a;
    }
    const int32 temp = find_root(father[a]);
    father[a] = temp;
    return temp;
}

Edge::Edge(const int32 a, const int32 b, const int32 weight) : a(a), b(b), weight(weight) {}

Go

package main

import (
    "fmt"
    "sort"
)

type UnionFind struct {
    father []int
}

type Edge struct {
    a int
    b int
    weight int
}

func main() {
    nodeCnt, edges := readData()

    result := kruskal(nodeCnt, edges)

    if result != nil {
        fmt.Println(*result)
    } else {
        fmt.Println("Not connected.")
    }
}

func readData() (nodeCnt int, edges []Edge) {
    var edgeCnt int
    fmt.Scan(&nodeCnt, &edgeCnt)

    edges = make([]Edge, edgeCnt)
    for i := range edgeCnt {
        var a, b, weight int
        fmt.Scan(&a, &b, &weight)
        edges[i] = Edge {
            a: a - 1,
            b: b - 1,
            weight: weight,
        }
    }

    return
}

func kruskal(nodeCnt int, edges []Edge) *int {
    sort.Slice(edges, func(i, j int) bool {
        return edges[i].weight < edges[j].weight
    })

    set := newUnionFind(nodeCnt)

    total := 0
    handled := 0
    for _, edge := range edges {
        if set.connected(edge.a, edge.b) {
            continue
        }
        set.merge(edge.a, edge.b)
        total += edge.weight
        handled++
        if handled == nodeCnt - 1 {
            return &total
        }
    }

    return nil
}

func newUnionFind(size int) *UnionFind {
    set := UnionFind {
        father: make([]int, size),
    }
    for i := range size {
        set.father[i] = i
    }
    return &set
}

func (set *UnionFind) connected(a, b int) bool {
    return set.findRoot(a) == set.findRoot(b)
}

func (set *UnionFind) merge(a, b int) {
    rootA := set.findRoot(a)
    rootB := set.findRoot(b)
    set.father[rootA] = rootB
}

func (set *UnionFind) findRoot(a int) int {
    if set.father[a] == a {
        return a
    }
    temp := set.findRoot(set.father[a])
    set.father[a] = temp
    return temp
}

Rust

struct UnionFind {
    father: Box<[i32]>,
}

struct Edge {
    a: i32,
    b: i32,
    weight: i32,
}

fn main() {
    let (node_cnt, mut edges) = read_data(&std::io::stdin());

    let result = kruskal(node_cnt, &mut edges);

    if let Some(res) = result {
        println!("{res}");
    } else {
        println!("Not connected.");
    }
}

fn read_data(reader: &std::io::Stdin) -> (usize, Vec<Edge>) {
    let mut line = String::new();
    reader.read_line(&mut line).unwrap();
    let mut iter = line.trim().split(' ').map(|x| x.parse::<usize>().unwrap());
    let node_cnt = iter.next().unwrap();
    let edge_cnt = iter.next().unwrap();

    let mut edges = vec![];
    for _i in 0..edge_cnt {
        let mut line = String::new();
        reader.read_line(&mut line).unwrap();
        let mut iter = line.trim().split(' ').map(|x| x.parse::<i32>().unwrap());
        let a = iter.next().unwrap();
        let b = iter.next().unwrap();
        let weight = iter.next().unwrap();
        edges.push(Edge {
            a,
            b,
            weight,
        });
    }

    (node_cnt, edges)
}

fn kruskal(node_cnt: usize, edges: &mut [Edge]) -> Option<i32> {
    edges.sort_by_key(|x| x.weight);

    let mut set = UnionFind::new(node_cnt);

    let mut total = 0;
    let mut handled = 0;
    for edge in edges {
        if set.connected(edge.a, edge.b) {
            continue;
        }
        set.merge(edge.a, edge.b);
        total += edge.weight;
        handled += 1;
        if handled == (node_cnt as i32) - 1 {
            return Some(total);
        }
    }

    None
}

impl UnionFind {
    fn new(size: usize) -> Self {
        let mut this = UnionFind {
            father: vec![0; size].into_boxed_slice(),
        };

        for i in 0..size {
            this.father[i] = i as i32;
        }

        this
    }

    fn connected(&mut self, a: i32, b: i32) -> bool {
        self.find_root(a) == self.find_root(b)
    }

    fn merge(&mut self, a: i32, b: i32) {
        let root_a = self.find_root(a);
        let root_b = self.find_root(b);
        self.father[root_a as usize] = root_b;
    }

    fn find_root(&mut self, a: i32) -> i32 {
        if self.father[a as usize] == a {
            a
        } else {
            let temp = self.find_root(self.father[a as usize]);
            self.father[a as usize] = temp;
            temp
        }
    }
}