从字符串使用看Golang和Rust对内存使用的区别
今天从Rust偶然回到Golang的世界,怎么写代码怎么别扭,总是忍不住在句子结尾加个分号…看到golang的字符串使用起来特别爽可以到处复制疯狂乱用,有一种从部队宿舍豆腐块被子的生活回归到居家肥宅的随意感,想起好久之前看的golang底层有关的内容,就写点东西来比较一下golang和rust对string的使用。
Go的字符串
在 Go 中,每个字符串本质上是一个结构体,其定义好了就不可用索引进行修改,包含两个字段:
- 指向字符串内容的指针(
*byte
):8 字节(在64 位架构下)。 - 字符串的长度(
len
):8 字节。
其内存布局是这个这样的结构体:
struct string {
data uintptr // 指向字符串内容的指针
len int // 字符串的长度
}
所以golang里面的一个string事实上占用的正真大小为:
package main
import (
"fmt"
"unsafe"
)
func main() {
str := "hello"
fmt.Printf("len(): %d bytes (content size)\n", len(str))
fmt.Printf("Sizeof string struct: %d bytes\n", unsafe.Sizeof(str))
fmt.Printf("Total estimated memory: %d bytes\n", unsafe.Sizeof(str)+uintptr(len(str)))
}
output:
String: hello
len(): 5 bytes (content size)
Sizeof string struct: 16 bytes
Total estimated memory: 21 bytes
Rust的字符串
String
rust里面有两种常用的字符串,一个是String,另一个是&str。
在Rust中,String
是一个可变的、堆分配的类型,底层实现是一个Vec<u8>
pub struct String {
vec: Vec<u8>,
}
所以一个String本质上还包含着vector的结构,也就是:
- 指向堆分配数据的指针:8 字节(在64 位系统上)。
- 字符串的长度(
usize
):8 字节。 - 堆分配容量(
usize
):8 字节。
所以说一个rust的string所占用的内存就至少是24字节,而且其本质由于就是一个vector,可以根据索引修改vector里面的值
fn main() {
let s = String::from("hello");
println!("Size of String struct: {} bytes", std::mem::size_of::<String>());
println!("Content length: {} bytes", s.len());
}
output:
Size of String struct: 24 bytes
Content length: 5 bytes
&str
另一个是&str
,在Rust中,&str
是一个字符串切片类型,它是对字符串数据的不可变引用。相比于String
,&str
更轻量级,因为它只是一个指向实际字符串数据的引用,而不是负责管理字符串数据本身。简单来说&str
就是一个对静态内存或者堆内存的一个引用。一个&str
的大小是固定的,包含两个部分:
- 指向字符串内容的指针(
*const u8
):8 字节(在 64 位系统上)。 - 字符串的长度(
usize
):8 字节。
所以一个&str至少就是16字节。
fn main() {
let s = "hello"; // &str 类型
println!("Size of &str: {} bytes", std::mem::size_of_val(&s));
println!("Content length: {} bytes", s.len());
}
output:
Size of &str: 16 bytes
Content length: 5 bytes
why
&str
和String
的关系
-
String
转换为&str
-
&str
是对String
数据的不可变引用。 -
通过
&
操作可以将String
转换为&str
,这并不是简单的取地址,而是生成一个指向String
内部数据的引用。 -
示例:
let s = String::from("hello"); let slice: &str = &s; //将 String 转为&str println!("{}", slice);
-
-
&str
转换为String
-
如果你需要一个拥有所有权的字符串,可以通过
.to_string()
或String::from()
将&str
转换为String
。 -
示例:
let slice: &str = "hello"; let s: String = slice.to_string(); // 将 &str 转为 String println!("{}", s);
-
怎么要两个字符串?
都有String了,为什么还要个这种&str,有时候看别人的代码都只创建&str而不是String,这是为什么呢?而且String还可以修改可以直接克隆。
- 轻量级和高效
-
内存开销更小:
&str
是不可变的引用,不需要额外的堆分配。- 它只包含一个指针和一个长度,总大小为16 字节,比
String
的24 字节更小。
-
数据共享:
&str
是对现有字符串数据的引用,不会创建新数据或重新分配内存。适用于只读场景,避免不必要的性能开销。- 例如,字符串字面量(
"hello"
)是静态分配的,用&str
表示效率更高。
-
性能优越:
-
在函数参数中使用
&str
而不是String
,避免堆分配和拷贝。 -
示例:
fn greet(name: &str) { println!("Hello, {}!", name); } let name = String::from("Alice"); greet(&name); // 传递不可变引用,避免拷贝,类似于golang里面传递&string
-
- 安全性
&str
的不可变性提供了额外的安全保障,确保引用的数据不会意外被修改。
3.适配静态字符串
-
如果数据是静态的(如程序中的字符串字面量),选择
&str
是合适的,经常作为全局静态变量使用let s: &str = "hello world"; // 静态字符串
结尾
总结对比
特性 | RustString | &str | Golangstring |
---|---|---|---|
大小 | 24字节 | 16字节 | 16字节 |
内存管理 | 动态分配堆内存 | 引用已有数据 | 堆分配 |
是否可变 | 可变 | 不可变 | 不可变 |
用途 | 动态字符串管理,修改内容 | 高效只读,数据共享 | 很多 |
典型场景 | 动态构建和管理字符串 | 静态字符串,函数参数,全局变量 | 很多 |