详解Go变量类型的内存布局
定义
每当我们编写任何程序时,我们都需要在内存中存储一些数据/信息。数据存储在特定地址的存储器中。内存地址看起来像0xAFFFF
(这是内存地址的十六进制表示)。
现在,要访问数据,我们需要知道存储它的地址。我们可以跟踪存储与程序相关的数据的所有内存地址。但想象一下,记住所有内存地址并使用它们访问数据会有非常困难。这就是为什么引入变量。
变量是一种占位符,用于引用计算机的内存地址,可理解为内存地址的标签。
什么是指针
指针是存储另一个变量的内存地址的变量。所以指针也是一种变量,只不过它是一种特殊变量,它的值存放的是另一个变量的内存地址。
在上面的例子中,指针p
包含值0x0001
,该值是变量的地址a
。
类型占用内存情况
unsafe包可以获取变量的内存使用情况
Go语言提供以下基本数字类型:
无符号整数 uint8,uint16,uint32,uint64
符号整数 int8,int16,int32,int64
实数 float32,float64 Predeclared
整数(和平台相关) uint,int,uintptr (指针)
32位系统
uint=uint32 int=int32 uintptr(指针)为32位的指针
64位系统
uint=uint64 int=int64 uintptr(指针)为64位的指针
Mac OS(64-Bit)示例:
package main import (
"fmt"
"unsafe"
) func main() {
var intValue int
var uint8Value uint8
var uint16Value uint16
var uint32Value uint32
var uint64Value uint64
var int8Value int8
var int16Value int16
var int32Value int32
var int64Value int64 var float32Value float32
var float64Value float64
var boolValue bool
var ptrValue uintptr
var complex64Value complex64
var complex128Value complex128
var strValue string
var byteValue byte
var runeValue rune
structValue := struct {
FieldA float32
FieldB string
}{, ""}
mapValue := map[int]int{}
var sliceValue []int
var intPtrValue *int
var chanValue chan int
var funcValue func() fmt.Println("funcValue = Size:", unsafe.Sizeof(funcValue) ) //size: 8
fmt.Println("chanValue = Size:", unsafe.Sizeof(chanValue) ) //size: 8
fmt.Println("intPtrValue = Size:", unsafe.Sizeof(intPtrValue) ) //size: 8
fmt.Println("sliceValue = Size:", unsafe.Sizeof(sliceValue) ) //size: 24
//type slice struct {
// array unsafe.Pointer
// len int
// cap int
//}
fmt.Println("mapValue = Size:", unsafe.Sizeof(mapValue) ) //size: 8
fmt.Println("structValue = Size:", unsafe.Sizeof(structValue) ) //size: 24
fmt.Println("strValue = Size:", unsafe.Sizeof(strValue), len(strValue)) //intValue = Size: 16, string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
//type stringStruct struct {
// str unsafe.Pointer
// len int
//}
fmt.Println("byteValue = Size:", unsafe.Sizeof(byteValue)) //intValue = Size: 1
fmt.Println("runeValue = Size:", unsafe.Sizeof(runeValue)) //intValue = Size: 4
fmt.Println("boolValue = Size:", unsafe.Sizeof(boolValue)) //intValue = Size: 1
fmt.Println("ptrValue = Size:", unsafe.Sizeof(ptrValue)) //intValue = Size: 8
fmt.Println("complex64Value = Size:", unsafe.Sizeof(complex64Value)) //intValue = Size: 8
fmt.Println("complex128Value = Size:", unsafe.Sizeof(complex128Value)) //intValue = Size: 16
fmt.Println("intValue = Size:", unsafe.Sizeof(intValue)) //intValue = Size: 8
fmt.Println("uint8Value = Size:", unsafe.Sizeof(uint8Value)) //uint8Value = Size: 1
fmt.Println("uint16Value = Size:", unsafe.Sizeof(uint16Value)) //uint16Value = Size: 2
fmt.Println("uint32Value = Size:", unsafe.Sizeof(uint32Value)) //uint32Value = Size: 4
fmt.Println("uint64Value = Size:", unsafe.Sizeof(uint64Value))// uint64Value = Size: 8 fmt.Println("int8Value = Size:", unsafe.Sizeof(int8Value)) //int8Value = Size: 1
fmt.Println("int16Value = Size:", unsafe.Sizeof(int16Value))//int16Value = Size: 2
fmt.Println("int32Value = Size:", unsafe.Sizeof(int32Value))//int32Value = Size: 4
fmt.Println("int64Value = Size:", unsafe.Sizeof(int64Value)) //int64Value = Size: 8 fmt.Println("float32Value = Size:", unsafe.Sizeof(float32Value)) //float32Value = Size: 4
fmt.Println("float64Value = Size:", unsafe.Sizeof(float64Value))//float64Value = Size: 8 }
# go run size.go
funcValue = Size:
chanValue = Size:
intPtrValue = Size:
sliceValue = Size:
mapValue = Size:
structValue = Size:
strValue = Size:
byteValue = Size:
runeValue = Size:
boolValue = Size:
ptrValue = Size:
complex64Value = Size:
complex128Value = Size:
intValue = Size:
uint8Value = Size:
uint16Value = Size:
uint32Value = Size:
uint64Value = Size:
int8Value = Size:
int16Value = Size:
int32Value = Size:
int64Value = Size:
float32Value = Size:
float64Value = Size:
上面的是基本类型,接下来了解下复杂类型,以结构体类型为例
type Example struct {
BoolValue bool
IntValue int16
FloatValue float32
}
该结构代表复杂类型。它代表7个字节,带有三个不同的数字表示。bool是一个字节,int16是2个字节,float32增加4个字节。但是,在此结构的内存中实际分配了8个字节。
所有内存都分配在对齐边界上,以最大限度地减少内存碎片整理。要确定对齐边界Go用于您的体系结构,您可以运行unsafe.Alignof函数。Go为64bit Darwin平台的对齐边界是8个字节。因此,当Go确定结构的内存分配时,它将填充字节以确保最终内存占用量是8的倍数。编译器将确定添加填充的位置。
什么是内存对齐呢?
内存对齐,也叫边界对齐(boundary alignment),是处理器为了提高处理性能而对存取数据的起始地址所提出的一种要求。编译器为了使我们编写的C程序更有效,就必须最大限度地满足处理器对边界对齐的要求。
从处理器的角度来看,需要尽可能减少对内存的访问次数以实现对数据结构进行更加高效的操作。为什么呢?因为尽管处理器包含了缓存,但它在处理数据时还得读取缓存中的数据,读取缓存的次数当然是越少越好!如上图所示,在采用边界对齐的情况下,当处理器需要访问a_变量和b_变量时都只需进行一次存取(图中花括号表示一次存取操作)。若不采用边界对齐,a_变量只要一次处理器操作,而b_变量却至少要进行两次操作。对于b_,处理器还得调用更多指令将其合成一个完整的4字节,这样无疑大大降低了程序效率。
以下程序显示Go插入到Example类型struct的内存占用中的填充:
package main import (
"fmt"
"unsafe"
) type Example struct {
BoolValue bool
IntValue int16
FloatValue float32
} func main() {
example := &Example{
BoolValue: true,
IntValue: ,
FloatValue: 3.141592,
} exampleNext := &Example{
BoolValue: true,
IntValue: ,
FloatValue: 3.141592,
} alignmentBoundary := unsafe.Alignof(example) sizeBool := unsafe.Sizeof(example.BoolValue)
offsetBool := unsafe.Offsetof(example.BoolValue) sizeInt := unsafe.Sizeof(example.IntValue)
offsetInt := unsafe.Offsetof(example.IntValue) sizeFloat := unsafe.Sizeof(example.FloatValue)
offsetFloat := unsafe.Offsetof(example.FloatValue) sizeBoolNext := unsafe.Sizeof(exampleNext.BoolValue)
offsetBoolNext := unsafe.Offsetof(exampleNext.BoolValue) fmt.Printf("example Size: %d\n", unsafe.Sizeof(example)) fmt.Printf("Alignment Boundary: %d\n", alignmentBoundary) fmt.Printf("BoolValue = Size: %d Offset: %d Addr: %v\n",
sizeBool, offsetBool, &example.BoolValue) fmt.Printf("IntValue = Size: %d Offset: %d Addr: %v\n",
sizeInt, offsetInt, &example.IntValue) fmt.Printf("FloatValue = Size: %d Offset: %d Addr: %v\n",
sizeFloat, offsetFloat, &example.FloatValue) fmt.Printf("Next = Size: %d Offset: %d Addr: %v\n",
sizeBoolNext, offsetBoolNext, &exampleNext.BoolValue) }
# go run alignment.go
example Size:
Alignment Boundary:
BoolValue = Size: Offset: Addr: 0xc000088000
IntValue = Size: Offset: Addr: 0xc000088002
FloatValue = Size: Offset: Addr: 0xc000088004
Next = Size: Offset: Addr: 0xc000088008
类型结构的对齐边界是预期的8个字节。
大小值显示将读取和写入该字段的内存量。正如所料,大小与类型信息一致。
偏移值显示进入内存占用的字节数,我们将找到该字段的开头。
地址是可以找到内存占用内每个字段的开头的地方。
我们可以看到Go在BoolValue和IntValue字段之间填充1个字节。偏移值和两个地址之间的差异是2个字节。您还可以看到下一个内存分配是从结构中的最后一个字段开始4个字节。
指针的使用
声明一个指针
使用以下语法声明类型为T的指针
var p *int
指针的零值是nil
。这意味着任何未初始化的指针都将具有该值nil
。让我们看一个完整的例子
package main
import "fmt" func main() {
var p *int
&p=1
}
注意:当指针没有指向的时候,不能对(*point)进行操作包括读取,否则会报空指针异常。
示例:
package main func main() {
var p *int *p = 1 //panic: runtime error: invalid memory address or nil pointer dereference }
解决方法即给该指针分配一个指向,即初始化一个内存,并把该内存地址赋予指针变量
示例:
import "fmt" func main() {
var p *int
var m int
p = &m
*p = 1
fmt.Println("m=", m)
fmt.Println("p=", p)
}
或还可以使用内置new()
函数创建指针。该new()
函数将类型作为参数,分配足够的内存以容纳该类型的值,并返回指向它的指针。
import "fmt" func main() {
var p *int p = new(int)
*p = 1
fmt.Println("p=", *p)
}
初始化指针
您可以使用另一个变量的内存地址初始化指针。可以使用&
运算符检索变量的地址
var x = 100
var p *int = &x
注意我们如何使用&
带变量的运算符x
来获取其地址,然后将地址分配给指针p
。
就像Golang中的任何其他变量一样,指针变量的类型也由编译器推断。所以你可以省略p
上面例子中指针的类型声明,并像这样写
var p = &a
取消引用指针
您可以*
在指针上使用运算符来访问存储在指针所指向的变量中的值。这被称为解除引用或间接
package main
import "fmt" func main() {
var a = 100
var p = &a fmt.Println("a = ", a)
fmt.Println("p = ", p)
fmt.Println("*p = ", *p)
}
输出:
a = 100
p = 0xc00004c080
*p = 100
您不仅可以使用*
运算符访问指向变量的值,还可以更改它。以下示例a
通过指针设置存储在变量中的值p
package main
import "fmt" func main() {
var a = 1000
var p = &a fmt.Println("a (before) = ", a) // Changing the value stored in the pointed variable through the pointer
*p = 2000 fmt.Println("a (after) = ", a)
}
输出:
a (before) = 1000
a (after) = 2000
指针指向指针
指针可以指向任何类型的变量。它也可以指向另一个指针。以下示例显示如何创建指向另一个指针的指针
package main
import "fmt" func main() {
var a = 7.98
var p = &a
var pp = &p fmt.Println("a = ", a)
fmt.Println("address of a = ", &a) fmt.Println("p = ", p)
fmt.Println("address of p = ", &p) fmt.Println("pp = ", pp) // Dereferencing a pointer to pointer
fmt.Println("*pp = ", *pp)
fmt.Println("**pp = ", **pp)
}
Go中没有指针算术
如果您使用过C / C ++,那么您必须意识到这些语言支持指针算法。例如,您可以递增/递减指针以移动到下一个/上一个内存地址。您可以向/从指针添加或减去整数值。您也可以使用关系运算符比较两个三分球==
,<
,>
等。
但Go不支持对指针进行此类算术运算。任何此类操作都将导致编译时错误
package main func main() {
var x = 67
var p = &x var p1 = p + 1 // Compiler Error: invalid operation
}
但是,您可以使用==
运算符比较相同类型的两个指针的相等性。
package main
import "fmt" func main() {
var a = 75
var p1 = &a
var p2 = &a if p1 == p2 {
fmt.Println("Both pointers p1 and p2 point to the same variable.")
}
}
Go中传递简单类型
import "fmt" func main() {
p := 5
change(&p)
fmt.Println("p=", p)//p= 0
}
func change(p *int) {
*p = 0
}
Go中所有的都是按值传递,对于复杂类型,传的是指针的拷贝
package main import "fmt" func main() {
var m map[string]int
m = map[string]int{"one": 1, "two": 2}
n := m
fmt.Printf("%p\n", &m) //0xc000074018
fmt.Printf("%p\n", &n) //0xc000074020
fmt.Println(m) // map[two:2 one:1]
fmt.Println(n) //map[one:1 two:2]
changeMap(m)
fmt.Printf("%p\n", &m) //0xc000074018
fmt.Printf("%p\n", &n) //0xc000074020
fmt.Println(m) //map[one:1 two:2 three:3]
fmt.Println(n) //map[one:1 two:2 three:3]
}
func changeMap(m map[string]int) {
m["three"] = 3
fmt.Printf("changeMap func %p\n", m) //changeMap func 0xc000060240
}
直接传指针 也是传指针的拷贝
package main import "fmt" func main() {
var m map[string]int
m = map[string]int{"one": 1, "two": 2}
n := m
fmt.Printf("%p\n", &m) //0xc000074018
fmt.Printf("%p\n", &n) //0xc000074020
fmt.Println(m) // map[two:2 one:1]
fmt.Println(n) //map[one:1 two:2]
changeMap(&m)
fmt.Printf("%p\n", &m) //0xc000074018
fmt.Printf("%p\n", &n) //0xc000074020
fmt.Println(m) //map[one:1 two:2 three:3]
fmt.Println(n) //map[two:2 three:3 one:1]
}
func changeMap(m *map[string]int) {
//m["three"] = 3 //这种方式会报错 invalid operation: m["three"] (type *map[string]int does not support indexing)
(*m)["three"] = 3 //正确
fmt.Printf("changeMap func %p\n", m) //changeMap func 0x0
}
指针类型和值类型
请记住这句话:七个小矮人(slice,map,func,channel,pointer, string, interface),自带魔法绳(指针), 所以没必要将它定义成引用类型。
这幅图中展示了常用的值类型和引用类型(引用类型和传引用是两个概念)。在左边是我们常用的一些值类型,函数调用时需要使用指针修改底层数据;而右边是“引用类型”,我们可以理解为它们的底层都是指针类型,所以右边的类型在使用的时候会有些不同,下文中会举例说明。
type Foo struct {
Name string
}
var bar = "hello biezhi"
// -------------方法返回值----------------
func returnValue() string {
return bar
}
func returnPoint() *string {
return &bar
}
// --------------方法入参-----------------
func modifyNameByPoint(foo *Foo) {
foo.Name = "emmmm " + foo.Name
}
func nameToUpper(foo Foo) string {
foo.Name = strings.ToUpper(foo.Name)
return foo.Name
}
// --------------实例方法-----------------
func (foo Foo) setName(name string) {
foo.Name = name
}
func (foo *Foo) setNameByPoint(name string) {
foo.Name = name
}
这里我列出了 3 组方法,分别是指针类型和值类型的示例。
总结
Go 不能进行指针运算 (明显降低复杂度和出错风险)
指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。
指针也是一种类型,不同于一般类型,指针的值是地址,这个地址指向其他的内存,通过指针可以读取其所指向的地址所存储的值。
函数方法的接受者,也可以是指针变量。
只声明未赋值的变量,默认会初始化为零值,int类是0,float类是0,bool是false, string是空串,复数类型如complex64、complex128,默认值为0+0i,[4]int是{0,0,0,0},引用类型和指针的零值都为nil,比如error默认是nil, nil类型还没有指向内存空间,不能直接赋值,因此需要通过new开辟一个内存地址,或指向一个已存在的变量地址。
- [N]int是数组,而[]int才是slice,即固定长度是数组,数组被分配在stack里,slice如果>32 kB就分配在heap里,同时stack速度比heap快,heap空间比stack大。
- 七个小矮人(slice,map,func,channel,pointer, string, interface),自带魔法绳(指针), 所以没必要将它定义成引用类型。
- struct、string实例,内存占用大则使用引用,小则无需引用
- 纯量(int类, float类, bool)无需使用引用
- 变量的生命周期越长则使用指针,否则使用值类型,防止stack区满了
- 不想被修改,或者需要并发安全,可以使用值拷贝
- 其他情况,优先用引用
参考资料
http://golang.org/doc/faq#Pointers
https://www.callicoder.com/golang-pointers/
https://www.ardanlabs.com/blog/2013/07/understanding-pointers-and-memory.html
https://www.ardanlabs.com/blog/2013/07/understanding-type-in-go.html
详解Go变量类型的内存布局的更多相关文章
- 详解js变量、作用域及内存
详解js变量.作用域及内存 来源:伯乐在线 作者:trigkit4 原文出处: trigkit4 基本类型值有:undefined,NUll,Boolean,Number和Strin ...
- MySQL数据类型 int(M) 表示什么意思?详解mysql int类型的长度值问题
MySQL 数据类型中的 integer types 有点奇怪.你可能会见到诸如:int(3).int(4).int(8) 之类的 int 数据类型.刚接触 MySQL 的时候,我还以为 int(3) ...
- LESS详解之变量(@)
变量基本上是每个语言脚本上都会涉及的一个小小知识点,是学好语言脚本的必经之路.LESS中也可以设置变量,然后通过变量可以改变整个网站的设计风格.良好的掌握LESS中变量的用法,是LESS的基础. 变量 ...
- C# 数据类型详解以及变量、对象与内存
学习刘铁猛老师<C#语言入门详解>视频,针对其中重点知识点进行总结. 1.什么是类型? 类型又称为数据类型(Data Type),数据类型在数据结构中的定义是一个值的集合以及定义在这个值集 ...
- 详解Python变量在内存中的存储
这篇文章主要是对python中的数据进行认识,对于很多初学者来讲,其实数据的认识是最重要的,也是最容易出错的.本文结合数据与内存形态讲解python中的数据,内容包括: 引用与对象 可变数据类型与不可 ...
- js面试题知识点全解(一变量类型和计算)
1.js中使用typeof能得到哪些类型 2.何时使用===和== 3.js中的内置函数 4.js变量按存储方式区分为哪些类型,并描述其特点 5.如何理解json 以下对这些问题的知识点做一些总结: ...
- linux free命令详解和使用实例(查看内存使用率)
转载:http://www.jb51.net/LINUXjishu/152017.html 1.命令格式: free [参数] 2.命令功能: free 命令显示系统使用和空闲的内存情况,包括物理内存 ...
- 匹夫细说C#:可以为null的值类型,详解可空值类型
首先祝大家中秋佳节快乐~ 0x00 前言 众所周知的一点是C#语言是一种强调类型的语言,而C#作为Unity3D中的游戏脚本主流语言,在我们的开发工作中能够驾驭好它的这个特点便十分重要.事实上,怎么强 ...
- Python之路-变量和基本数据类型详解(变量、数据类型、)
一.注释 注释的作用: 增加程序的可读性 作为调试用 提高团队的合作效率 注释的分类 1.单行注释 以井号(#)开头,右边的所有内容当做说明 2.多行注释 以三对单引号(’’’注释内容’’’)将注释包 ...
随机推荐
- linux下各文件夹的结构说明及用途介绍:Linux目录结构介绍
linux下各文件夹的结构说明及用途介绍: /bin:二进制可执行命令. /dev:设备特殊文件. /etc:系统管理和配置文件. /etc/rc.d:启动的配 置文件和脚本. /home:用户主目录 ...
- 还是畅通工程 HDU - 1233
题目链接:https://vjudge.net/problem/HDU-1233 思路: 最小生成树板子. #include <iostream> #include <stdio.h ...
- Rust中的Rc--引用计数智能指针
大部分情况下所有权是非常明确的:可以准确的知道哪个变量拥有某个值.然而,有些情况单个值可能会有多个所有者.例如,在图数据结构中,多个边可能指向相同的结点,而这个结点从概念上讲为所有指向它的边所拥有.结 ...
- MySQL数据库 外键,级联, 修改表的操作
1.外键: 用来建立两张表之间的关系 - 一对多 - 多对多 - 一对一 研究表与表之间的关系: 1.定义一张 员工部门表 id, name, gender, dep_name, dep_desc - ...
- opencv归一化图像
cv::normalize(src, dst, 0.0, 255.0, cv::NORM_MINMAX);
- linux中以.d结尾的目录
一般为了保持对原有配置方式的兼容,而增加的.d结尾目录. 如: /etc/X11/xorg.conf 这原本是个文件,现在也有了一个/etc/X11/xorg.conf.d这样的目录,显卡驱动的相关设 ...
- Nginx配置文件nginx.conf(八)
原文链接:https://www.cnblogs.com/knowledgesea/p/5175711.html 在nginx.conf的注释符号是#. 默认的nginx.conf内容为: #user ...
- Leetcode142 环形链表
很多题解没有讲清楚非环部分的长度与相遇点到环起点那部分环之间为何是相等的这个数学关系.这里我就补充下为何他们是相等的.假设非环部分的长度是x,从环起点到相遇点的长度是y.环的长度是c.现在走的慢的那个 ...
- 微信小程序实现图片放大预览效果
可以直接用微信程序自己的api很方便的实现 核心方法 wx.previewImage: 直接上代码, wxml: <!--pages/prewpicture/prew.wxml--> &l ...
- 阿里邮箱地址,smpt
企业邮箱的POP3.SMTP.IMAP地址是什么? 企业邮箱POP.SMTP.IMAP地址列表如下: (阿里云邮箱web端通用访问地址:https://qiye.aliyun.com/),客户端推荐以 ...