当前位置: 首页 > news >正文

golang--数据类型与存储

在 Go 语言中,理解值类型(value types)和引用类型(reference types)的区别对于编写高效、正确的代码至关重要。以下是主要的区别点和需要注意的特殊情况:

一、值类型(Value Types)

包含的类型

  • 基本数据类型(bool, int, float, complex, string 等)
  • 数组(array
  • 结构体(struct

核心特点:

1. 直接存储值

a := 42
b := a  // 创建 a 的副本(值复制)
b = 10  // 修改 b 不影响 a
fmt.Println(a) // 42

2. 传参时复制整个值

func modify(arr [3]int) {arr[0] = 100
}
original := [3]int{1, 2, 3}
modify(original)
fmt.Println(original) // [1 2 3](未改变)

内存存储

通常分配在栈上(小对象),但可能逃逸到堆(如函数返回局部变量地址时)。

类型存储方式大小特点
bool直接存储(true=1,false=0)1字节零值=false
整数类型直接存储二进制值int8/16/32/64支持位操作
浮点数IEEE-754 标准float32(4B)/64(8B)精确计算需用 math/big
complex实部+虚部存储8/16字节complex128 精度更高
array连续内存块len*元素大小长度固定,类型签名包含长度

示例:

// 数组存储示例
arr := [3]int{1, 2, 3}
// 内存布局:[0x01, 0x00, 0x00, 0x00, 0x02, ...] (小端序)

3. 内存分配在栈上(小对象)

  • 小对象(如结构体)通常在栈上分配,速度更快

4. string 的特殊性

  • 共享只读
   s1 := "hello"s2 := s1 // 虽然 string 是值类型,但底层共享只读字节数组// 修改会触发新内存分配(不可变性)
  • 底层字节数组不可变:
s := "hello"
// s[0] = 'H' // 编译错误(禁止修改)
s2 := s          // 复制描述符(8+8=16字节),共享底层数据
s3 := s + "world" // 新建底层数组(复制+追加)
  • 子串零成本​​:截取子串不需要复制数据
    截取子字符串(如s[i:j])时,会创建一个新的字符串头,其中Data指向原字符串的相应位置(即原起始地址加上偏移量i),长度设置为j-i。因此,子字符串和原字符串共享一部分底层数组。

5. 比较支持

type Point struct{ X, Y int }
p1 := Point{1, 2}
p2 := Point{1, 2}
fmt.Println(p1 == p2) // true(可比较)

String

Unicode库,判断字符的类型


其中 v 代表字符):
判断是否为字母: unicode.IsLetter(v)
判断是否为十进制数字: unicode.IsDigit(v)
判断是否为数字: unicode.IsNumber(v)
判断是否为空白符号: unicode.IsSpace(v)
判断是否为Unicode标点字符 :unicode.IsPunct(v)

取出一个字符串中的字符串和数值字符串
得到map[ddgm:[495 468] fdfsf:[123.4 1.2 11] dg:[49151]]

str:="fdfsf,123.4,1.2,11,dg,49151,ddgm,495,468"
istMap :=  make(map[string][]string)
start := 0var key stringvar value []stringvar tmp stringvar tmpArr []stringfor index, v := range instruction {if string(v) == "," && index != len(instruction)-1 && unicode.IsLetter(rune(instruction[index+1])) { //标点和结束tmp = instruction[start:index]tmpArr = strings.Split(tmp, ",")key = tmpArr[0]value = tmpArr[1:]istMap[key] = valuestart = index + 1}if index == len(str)-1 { //数值tmp = str[start : index+1]tmpArr = strings.Split(tmp, ",")key = tmpArr[0]value = tmpArr[1:]istMap[key] = valuestart = index + 1}}

只读共享

	s := "abcdef"s1 := sfmt.Printf("s指针地址: %p\n", &s)fmt.Printf("s1指针地址: %p\n", &s1)fmt.Printf("s底层数据地址: %p\n", unsafe.StringData(s))fmt.Printf("s1底层数据地址: %p\n", unsafe.StringData(s1))//(只读共享)// 修改操作会触发新分配s1 += " world"fmt.Printf("s指针地址: %p\n", &s)fmt.Printf("s1指针地址: %p\n", &s1)fmt.Printf("s底层数据地址: %p\n", unsafe.StringData(s))fmt.Printf("s1底层数据地址: %p\n", unsafe.StringData(s1))
/*
s指针地址: 0xc00023aab0
s1指针地址: 0xc00023aac0
s底层数据地址: 0x184b115
s1底层数据地址: 0x184b115
s指针地址: 0xc00023aab0
s1指针地址: 0xc00023aac0
s底层数据地址: 0x184b115
s1底层数据地址: 0xc000213120
*/

如何实现的只读特性?

底层数据结构

字符串在运行时表示为:

type StringHeader struct {Data uintptr // 指向底层字节数组的指针Len  int     // 字符串长度
}
  • Data指向只读内存区域
  • 无修改字符串内容的操作接口
编译器级别的保护

编译错误

s := "hello"
s[0] = 'H' // 编译错误: cannot assign to s[0]
运行时保护

运行时机制

  1. 只读内存段
  • 字符串字面量存储在二进制文件的.rodata(只读数据段)
  • 程序加载时,操作系统将其映射到只读内存页
  1. 写保护内存页
    现代操作系统对只读内存页设置写保护:
内存页权限:
.rodata 段: R-- (只读不可写)
.data 段: RW- (可读写)
.text 段: R-X (可读可执行)
  1. 硬件级保护
  • CPU 内存管理单元(MMU)拦截非法写操作
  • 触发操作系统级保护异常(SIGSEGV)

二、引用类型(Reference Types)

包含的类型

  • 切片(slice
  • 映射(map
  • 通道(channel
  • 函数(func
  • 指针(pointer
  • 接口(interface

核心特点:

  1. 存储的是引用(指针)

    m1 := map[string]int{"a": 1}
    m2 := m1  // 复制引用(共享底层数据)
    m2["a"] = 100
    fmt.Println(m1["a"]) // 100(值被修改)
    
  2. 零值为 nil

    var s []int        // nil slice
    var m map[string]int // nil map
    // 操作 nil 引用会导致运行时错误
    
  3. 不可直接比较

    s1 := []int{1,2}
    s2 := []int{1,2}
    // fmt.Println(s1 == s2) // 编译错误(slice 不可比较)
    // 只能与 nil 比较: fmt.Println(s1 == nil)
    
  4. 函数传递效率高

    func process(slice []int) {// 只传递 24 字节的切片头(ptr+len+cap)
    }
    data := make([]int, 1000000) // 底层数组很大
    process(data)               // 高效传递
    
  5. 共享底层数据风险

    original := []int{1,2,3,4}
    sub := original[:2] // 共享同一个底层数组
    sub[0] = 99
    fmt.Println(original[0]) // 99(意外修改!)
    

内存存储

类型底层结构描述符大小特点
slice{ptr *T, len int, cap int}24字节cap ≥ len,可动态增长
map指向 runtime.hmap 的指针8字节哈希桶+溢出链
chan指向 runtime.hchan 的指针8字节环形队列+同步原语
func函数入口地址指针8字节闭包捕获外部变量
pointer目标内存地址8字节可指向任意类型
interface{_type *rtype, data unsafe.Pointer}16字节动态分发基础

需要特别注意的场景

1. 切片扩容陷阱

s := make([]int, 2, 4) // [0,0] 容量4
s1 := s[:2]           // 共享底层数组s = append(s, 5)     // 容量够,未扩容
s1[0] = 1            // 修改共享数组
fmt.Println(s[0])    // 1(被修改)s = append(s, 6,7)   // 超过容量,新建数组
s1[0] = 2           // 不再影响 s
fmt.Println(s[0])    // 1(未改变)

2. Map 并发访问危险

m := make(map[int]int)
go func() {for { m[1]++ } // 并发写
}()
go func() {for { _ = m[1] } // 并发读
}()
// 可能触发 fatal error: concurrent map read and map write

解决方案

  • 使用 sync.Mutexsync.RWMutex
  • 使用 sync.Map(Go 1.9+)

3. 接口的特殊行为

var w io.Writer = os.Stdout
w.Write([]byte("hello")) // 正确var w2 io.Writer
// w2.Write(...)  // 运行时 panic: nil pointer

关键点

  • 接口变量存储 (type, value)
  • 值为 nil 但类型非空的接口不等于 nil
    var buf *bytes.Buffer
    var w io.Writer = buf
    fmt.Println(w == nil) // false!(类型为 *bytes.Buffer)
    

4. 指针接收者与方法

type Counter struct{ n int }func (c *Counter) Inc() { c.n++ } // 指针接收者c := Counter{}
c.Inc()    // 自动转换为 (&c).Inc()
fmt.Println(c.n) // 1

规则

  • 值类型可调用指针接收者方法(Go 自动取地址)
  • 指针类型可调用值接收者方法(Go 自动解引用)

性能优化建议

  1. 大结构体用指针传递

    type LargeStruct struct { data [1024]byte }// 避免复制开销
    func (s *LargeStruct) Process() {}
    
  2. 避免不必要的堆分配

    // 不佳:返回指针导致堆分配
    func newPoint() *Point { return &Point{x: 1} }// 推荐:返回值(可能栈分配)
    func newPoint() Point { return Point{x: 1} }
    
  3. 预分配切片/映射容量

    // 避免频繁扩容
    users := make([]User, 0, 1000)
    cache := make(map[string]int, 100)
    

特殊类型指南

类型值/引用比较复制行为注意要点
数组深拷贝传参效率低
切片引用复制引用小心共享数据和扩容
Map引用复制引用非并发安全,需加锁
通道引用✅*复制引用比较相同通道对象
接口引用复制描述符有运行时开销
函数引用复制函数指针可作一等公民使用
字符串复制描述符底层数据只读共享

(*) 通道可比较:相同通道实例比较为 true

总结关键点

  1. 修改行为:引用类型会修改所有引用同一数据的变量
  2. 零值处理:引用类型零值为 nil,需显式初始化
  3. 并发安全:基本值类型原子操作安全,引用类型需要同步
  4. 性能取舍
    • 小对象:优先用值类型(栈分配)
    • 大对象:用指针或引用类型(避免复制)
  5. 比较限制:切片、map、函数等不可比较
  6. 接口陷阱nil 接口 != nil 具体值

理解这些差异可以帮助你避免常见陷阱(如意外数据共享、nil指针panic)并编写更高效的Go代码。

三、各个类型的指针操作

1. 基础指针操作

var a int = 42
p := &a  // 获取地址// 解引用操作
*p = 100  // a 变为 100
fmt.Println(a == *p) // true

2. 结构体指针优化

type Point struct{ X, Y float64 }// 直接通过指针访问字段(编译器自动优化)
p := &Point{1, 2}
p.Y = 3 // 等价于 (*p).Y = 3

3. 切片指针操作

data := []int{1, 2, 3}
ptr := &data[0]   // 获取首元素地址
*ptr = 100        // data[0] = 100// 危险操作:访问越界元素
// badPtr := &data[5]  // 编译通过但运行时 panic

4. unsafe 高级指针操作

import "unsafe"type Secret struct {id   int32flag uint16
}s := Secret{1024, 0xABCD}
ptr := unsafe.Pointer(&s)// 访问结构体内部字段
idPtr := (*int32)(ptr)          // 获取 id 字段指针
flagPtr := (*uint16)(unsafe.Pointer(uintptr(ptr) + unsafe.Offsetof(s.flag)))fmt.Println(*idPtr)   // 1024
fmt.Printf("%X", *flagPtr) // ABCD

四、各类型特殊注意事项

1. 字符串:只读字节序列

s := "hello"
// s[0] = 'H' // 编译错误:不可修改// 安全转换:string ↔ []byte
bytes := []byte(s)  // 复制数据创建新切片
str := string(bytes) // 同样复制数据

2. 切片:三大核心陷阱

陷阱 1:共享底层数组

original := []int{1,2,3,4,5}
sub := original[1:3] // 共享底层数组sub[0] = 100 // 修改影响 original[1]
fmt.Println(original) // [1,100,3,4,5]

陷阱 2:append 自动扩容

s := make([]int, 2, 3) // len=2, cap=3
s1 := append(s, 1)    // 共用底层数组
s2 := append(s, 2)    // 仍然共用到 cap=3s2[0] = 100           // 意外修改 s 和 s1
fmt.Println(s[0])     // 100(预期为 0)

陷阱 3:空切片 vs nil 切片

var nilSlice []int      // nil,与 nil 相等
emptySlice := []int{}   // 非 nil,已分配描述符fmt.Println(nilSlice == nil)   // true
fmt.Println(emptySlice == nil)  // false

3. Map:特殊的引用类型

m := make(map[string]int)
m["a"] = 1// 错误:禁止取元素地址
// p := &m["a"]  // 编译错误:无法获取地址// 正确访问方式
val, exists := m["a"]

4. 接口:双重指针设计

var w io.Writer
w = os.Stdout     // 存储 {*os.File类型信息, *os.File值指针}// nil 接口 != nil 具体值
var buf *bytes.Buffer
w = buf            // w != nil(类型信息非空)
if w == nil {      // false /* ... */ 
}

五、高效内存操作指南

1. 内存复用技巧

// 重用切片内存(避免重复分配)
pool := make([]*Object, 0, 100)func getObject() *Object {if len(pool) > 0 {obj := pool[len(pool)-1]pool = pool[:len(pool)-1]return obj}return &Object{}
}

2. 零拷贝转换(unsafe 实现)

// string → []byte(零拷贝)
func stringToBytes(s string) []byte {return *(*[]byte)(unsafe.Pointer(&struct {s stringc int}{s, len(s)},))
}
// 注意:结果切片只读!

3. 避免意外内存泄漏

func process() {bigData := make([]byte, 10<<20) // 10MB// 切片截取导致大内存无法回收smallPart := bigData[:10]// 解决方案:复制需要的数据result := make([]byte, 10)copy(result, bigData[:10])
} // 整个 10MB 可被回收

六、指针操作安全规范

1. 禁止指针运算(除 unsafe)

   arr := [3]int{1,2,3}p := &arr[0]// p++ // 禁止:Go 不支持指针算术

2. 内存对齐检查

   type BadLayout struct {a bool    // 1字节b int64   // 8字节 (需要7字节填充)}             // 总大小16字节而非9字节

3. cgo 指针安全

   /*#include <stdlib.h>*/import "C"import "unsafe"func copyToC(data []byte) {cptr := C.malloc(C.size_t(len(data)))defer C.free(cptr)// 通过unsafe转换C.memcpy(cptr, unsafe.Pointer(&data[0]), C.size_t(len(data)))}

4. 引用类型禁止取元素地址

m := map[int]string{1: "one"}
// 以下操作非法!因为map元素可能被重新散列迁移
// p := &m[1]

5. 切片的安全操作

s := []int{1,2,3}
first := &s[0] // 允许取元素地址
*first = 100   // 合法操作(底层数组稳定)

七、性能优化对照表

操作推荐方式避免方式性能提升
大结构体传参func(p *Struct)func(s Struct)8x+
小结构体传参func(s Struct)func(p *Struct)15-20%
大切片传递func(s []T)func(arr [10000]T)10000x
临时对象创建sync.Pool重复 new3-5x
字符串拼接strings.Builder+ 操作符10x+
Map 初始化m := make(map[K]V, hint)无预设容量2-3x

存储,指针操作总结

  1. 存储本质

    • 值类型:直接存储数据
    • 引用类型:存储描述符(指针+元数据)
    • 特殊类型:字符串只读、接口双层指针
  2. 指针安全

    • 常规代码避免使用 unsafe
    • 禁止取 map 元素地址
    • 注意切片共享的陷阱
  3. 性能关键

    • 大对象用指针传递
    • 预分配切片/map容量
    • 避免不必要的数据复制
  4. 内存管理

    • 理解逃逸分析机制
    • 复用内存(sync.Pool)
    • 避免因切片截取导致内存泄漏

八、不同类型的重点

切片扩容

Go语言在runtime/slice.go中的实现(go版本1.24),切片扩容的规则可以总结如下:

  1. 核心函数growslice
func growslice(oldPtr unsafe.Pointer, newLen, oldCap, num int, et *_type) slice
  • oldPtr: 原切片底层数组指针
  • newLen: 扩容后的新长度
  • oldCap: 原切片容量
  • num: 新增元素数量
  • et: 元素类型信息
  1. 切片(slice)扩容容量计算的函数
// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {//首先检查新长度是否超过旧容量的2倍,如果是则直接返回新长度newcap := oldCapdoublecap := newcap + newcapif newLen > doublecap {return newLen}
//对于容量小于256的小切片,采用双倍扩容策略const threshold = 256if oldCap < threshold {return doublecap}/*对于大切片,采用平滑过渡策略:
初始增长因子约为1.25倍
通过位运算>>2实现快速除以4
循环直到找到足够大的容量
*/for {// Transition from growing 2x for small slices// to growing 1.25x for large slices. This formula// gives a smooth-ish transition between the two.newcap += (newcap + 3*threshold) >> 2// We need to check `newcap >= newLen` and whether `newcap` overflowed.// newLen is guaranteed to be larger than zero, hence// when newcap overflows then `uint(newcap) > uint(newLen)`.// This allows to check for both with the same comparison.if uint(newcap) >= uint(newLen) {break}}// Set newcap to the requested cap when// the newcap calculation overflowed.//如果计算过程中出现溢出(负数),则直接返回新长度if newcap <= 0 {return newLen}return newcap
}

扩容策略

  1. 首先检查新长度是否超过旧容量的2倍,如果是则直接返回新长度
  2. 对于容量小于256的小切片,采用双倍扩容策略
  3. 对于大切片,采用平滑过渡策略:
  • 初始增长因子约为1.25倍
  • 通过位运算>>2实现快速除以4
  • 循环直到找到足够大的容量
  1. 如果计算过程中出现溢出(负数),则直接返回新长度

相关文章:

  • D包和模块.go
  • Spring Boot + AOP + Jasypt,3 步实现敏感数据脱敏
  • Java 实现后端调用 Chromium 浏览器无头模式截图的方案
  • Java web非Maven项目中引入EasyExcel踩坑记录
  • 批量创建tmux tmux批量
  • 深入解析 Java List 实现类的底层原理
  • 腾讯云TCCA认证考试报名 - TDSQL数据库交付运维工程师(PostgreSQL版)
  • 12.9 定时任务
  • SkyWalking 部署与应用(Windows)
  • 3DS 转换为 STP 全攻略:迪威模型网在线转换详解
  • OpenAI 如何在激烈的AI人才争夺战中抢占先机?
  • 视频或视频流和帧的关系?怎么理解?
  • MATLAB R2025a安装教程
  • 2025 MWC 上海盛大开幕,聚焦AI、5G-Advanced及开放API
  • Git工作流程及使用规范
  • GoByExample简单应用
  • Vue3 项目国际化实践
  • 标杆确立!永洪科技位于IDC报告Data Analytics领域象限排头位!
  • Oracle 查看所有表的字段名、数据类型及长度
  • Android软件适配遥控器需求-案例经验分享
  • wordpress 友言/徐州关键词优化排名
  • 图书馆网站制作/seo原创工具
  • 做国际黄金的网站/青岛app开发公司
  • 免费写作网站/kol合作推广
  • 设计网站怎样做色卡/外贸网站大全
  • 网络维护员是干什么的/电商seo是什么