1. 复合数据类型
Go
语言基本的复合数据类型有指针、数组、切片、字典、通道、结构和接口等。格式如下:
* pointerType // 指针类型,
[n]elementType // 数组类型,
[]elementType // 切片类型,
map [keyType]valueType // 字典类型
chan valueType // 通道类型
// 结构体类型
struct {
fieldType fieldType
fieldType fieldType
...
}
// 接口类型
interface {
method1(inputParams) (returnParams)
method2(inputParams) (returnParams)
...
}
2. 指针定义
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go 语言中使用在变量名前面添加&
操作符(前缀)来获取变量的内存地址(取地址操作),格式如下:
ptr := &v // v 的类型为 T
其中 v
代表被取地址的变量,变量 v
的地址使用变量 ptr
进行接收, ptr
的类型为*T
,称做 T 的指针类型,*
代表指针。
Go
语言的取地址符是 &
,放到一个变量前使用就会返回相应变量的内存地址。
获取变量在内存中地址:
package main
import "fmt"
func main() {
var a int = 10
fmt.Printf("变量的地址: %x\n", &a) // 变量的地址: c000018068
fmt.Printf("%p", &a) // %p 打印变量内存地址,指针的值是带有0x十六进制前缀的一组数据。
}
一个指针变量指向了一个值的内存地址。在使用指针前你需要声明指针。指针声明格式如下:
var varName *varType
varType
为指针类型, varName
为指针变量名, *
号用于指定变量是作为一个指针。以下是有效的指针声明:
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
指针类型是依托某一个类型而存在的,比如:一个整型为 int
,那么它对应的整型指针就是 *int
,也就是在 int
的前面加上一个星号。没有 int
类型,就不会有 *int
类型。而 int
也被称为 *int
指针类型的基类型。
指针类型的这个定义:如果我们拥有一个类型 T
,那么以 T
作为基类型的指针类型为 *T
。
不过 Go
中也有一种指针类型是例外,它不需要基类型,它就是 unsafe.Pointer
。unsafe.Pointer
类似于 C
语言中的 void*
,用于表示一个通用指针类型,也就是任何指针类型都可以显式转换为一个 unsafe.Pointer
,而 unsafe.Pointer
也可以显式转换为任意指针类型,如下面代码所示:
var p *T
var p1 = unsafe.Pointer(p) // 任意指针类型显式转换为unsafe.Pointer
p = (*T)(p1) // unsafe.Pointer也可以显式转换为任意指针类型
从图中我们看到,Go
为指针变量 p
分配的内存单元中存储的是整型变量 a
对应的内存单元的地址。也正是由于指针类型变量存储的是内存单元的地址,指针类型变量的大小与其基类型大小无关,而是和系统地址的表示长度有关。
它所指向的值的内存地址在 32 和 64 位机器上分别占用 4
或 8
个字节,占用字节的大小与所指向的值的大小无关。
package main
import "unsafe"
type foo struct {
id string
age int8
addr string
}
func main() {
var p1 *int
var p2 *bool
var p3 *byte
var p4 *[20]int
var p5 *foo
var p6 unsafe.Pointer
println(unsafe.Sizeof(p1)) // 8
println(unsafe.Sizeof(p2)) // 8
println(unsafe.Sizeof(p3)) // 8
println(unsafe.Sizeof(p4)) // 8
println(unsafe.Sizeof(p5)) // 8
println(unsafe.Sizeof(p6)) // 8
}
unsafe
包的 Sizeof
函数原型如下:
func Sizeof(x ArbitraryType) uintptr
在 Go
语言中 uintptr
类型的大小就代表了指针类型的大小。
当一个指针被定义后没有分配到任何变量时,它的默认值为 nil
。指针变量通常缩写为 ptr
。
变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。
多个指针变量可以指向同一个变量的内存单元的,这样通过其中一个指针变量对内存单元的修改,是可以通过另外一个指针变量的解引用反映出来的,比如下面例子:
var a int = 5
var p1 *int = &a // p1指向变量a所在内存单元
var p2 *int = &a // p2指向变量b所在内存单元
(*p1) += 5 // 通过p1修改变量a的值
println(*p2) // 10 对变量a的修改可以通过另外一个指针变量p2的解引用反映出来
3. 指针使用
指针使用流程:
- 定义指针变量
- 为指针变量赋值
- 访问指针变量中指向地址的值
在指针类型前面加上 *
号(前缀)来获取指针所指向的内容。
使用示例:
package main
import "fmt"
func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}
输出结果为:
a 变量的地址是: 20818a220
ip 变量储存的指针地址: 20818a220
*ip 变量的值: 20
以下几点使用指针的建议:
-
不要对
map
、slice
、channel
这类引用类型使用指针; -
如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
-
如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
-
如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
-
像
int
、bool
这样的小数据类型没必要使用指针; -
如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
-
指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然
Go
语言允许这么做,但是这会使你的代码变得异常复杂;
4. 指针特点
-
在赋值语句中,
*p
出现在=
左边表示是指针声明,*p
出现在=
右边表示取指针指向的值; -
结构体指针访问结构体字段仍然使用
.
点操作符,Go
不支持->
操作符; -
Go
不支持指针的运算;
在 C
语言中,我们可以通过指针运算实现各种高级操作,比如简单的数组元素的遍历:
#include <stdio.h>
int main() {
int a[] = {1, 2, 3, 4, 5};
int *p = &a[0];
for (int i = 0; i < sizeof(a)/sizeof(a[0]); i++) {
printf("%d\n", *p);
p = p + 1;
}
}
Go
在语法层面抛弃了指针运算这个特性。下面的代码将得到 Go
编译器的报错信息
package main
func main() {
var arr = [5]int{1, 2, 3, 4, 5}
var p *int = &arr[0]
println(*p)
p = p + 1 // 编译器报错:cannot convert 1 (untyped int constant) to *int
println(*p)
}
如果我们非要做指针运算,Go
依然提供了 unsafe
的途径,比如下面通过 unsafe
遍历数组的代码:
package main
import "unsafe"
func main() {
var arr = [5]int{11, 12, 13, 14, 15}
var p *int = &arr[0]
var i uintptr
for i = 0; i < uintptr(len(arr)); i++ {
p1 := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(p)) + i*unsafe.Sizeof(*p)))
println(*p1)
}
}
上面这段代码就通过 unsafe.Pointer
与 uintptr
的相互转换,间接实现了“指针运算”。但即便我们可以使用 unsafe
方法实现“指针运算”,Go
编译器也不会为开发人员提供任何帮助,开发人员需要自己告诉编译器要加减的绝对地址偏移值,而不是像前面 C
语言例子中那样,可以根据指针类型决定指针运算中数值 1 所代表的实际地址偏移值。
- 函数中允许返回变量的地址;
package main
import "fmt"
// Phone is struct type 注释开头必须是 结构体或者方法的名字,后面再加空格
type Phone struct { //exported type Phone should have comment or be unexportedgo-lint
model string
color string
price int
}
func main() {
var a *int = nil
var b *int
c := 10
b = &c
e := *b
phone := Phone{"huawei", "blue", 1000}
p := &phone
// p++ invalid operation: p++ (non-numeric type *Phone)
fmt.Printf("a is %v, a type is %T\n", a, a)
fmt.Printf("b is %v, b type is %T\n", b, b)
fmt.Printf("e is %v, e type is %T\n", e, e)
fmt.Printf("p is %v, p type is %T\n", p, p)
fmt.Printf("p.model is %v, p.price is %v\n", p.model, p.price)
result := sum(5, 3)
fmt.Printf("result is %v, result type is %T\n", *result, result)
}
func sum(a, b int) *int {
ret := a + b
return &ret
}
输出结果:
a is <nil>, a type is *int
b is 0xc000016068, b type is *int
e is 10, e type is int
p is &{huawei blue 1000}, p type is *main.Phone
p.model is huawei, p.price is 1000
result is 8, result type is *int
-
对变量进行取地址操作使用
&
操作符,可以获得这个变量的指针变量; -
对指针变量进行取值操作使用
*
操作符,可以获得指针变量指向的原变量的值; -
限制了显式指针类型转换;
在 C 语言中,我们可以像下面代码这样实现显式指针类型转换:
#include <stdio.h>
int main() {
int a = 0x12345678;
int *p = &a;
char *p1 = (char*)p; // 将一个整型指针显式转换为一个char型指针
printf("%x\n", *p1);
}
但是在 Go 中,这样的显式指针转换会得到 Go 编译器的报错信息:
package main
import (
"fmt"
"unsafe"
)
func main() {
var a int = 0x12345678
var pa *int = &a
var pb *byte = (*byte)(pa) // 编译器报错:cannot convert pa (variable of type *int) to type *byte
fmt.Printf("%x\n", *pb)
}
如果我们“一意孤行”,非要进行这个转换,Go
也提供了 unsafe
的方式,因为我们需要使用到 unsafe.Pointer
,如下面代码:
func main() {
var a int = 0x12345678
var pa *int = &a
var pb *byte = (*byte)(unsafe.Pointer(pa)) // ok
fmt.Printf("%x\n", *pb) // 78
}
如果我们使用 unsafe
包中类型或函数,代码的安全性就要由开发人员自己保证,也就是开发人员得明确知道自己在做啥!
5. Go 空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil
。 nil
指针也称为空指针。 nil
在概念上和其它语言的 null
、 None
、 nil
、 NULL
一样,都指代零值或空值。
空指针值为 nil,即没有内存地址,是不能进行赋值操作的,比如下面的示例:
var intP *int
*intP =10
运行的时候会提示 invalid memory address or nil pointer dereference
。这时候该怎么办呢?其实只需要通过 new
函数给它分配一块内存就可以了,如下所示:
var intP *int = new(int)
//更推荐简短声明法,这里是为了演示
//intP:=new(int)
一个指针变量通常缩写为 ptr
。空指针判断方法:
if(ptr != nil) /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */
使用示例:
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("ptr 的值为 : %x\n", ptr )
}
输出结果为:
ptr 的值为 : 0
6. Go 指针数组
ptr
为整型指针数组。因此每个元素都指向了一个值。以下实例的三个整数将存储在指针数组中:
package main
import "fmt"
const MAX int = 3
func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int; // 声明了整型指针数组
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
}
}
7. Go 指向指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址:
访问指向指针的指针变量值需要使用两个 *
号,如下所示:
package main
import "fmt"
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指针 ptr 地址 */
ptr = &a
/* 指向指针 ptr 地址 */
pptr = &ptr
/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a )
fmt.Printf("指针变量 *ptr = %d\n", *ptr )
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
package main
func main() {
var a int = 5
var p1 *int = &a
println(*p1) // 5
var b int = 55
var p2 *int = &b
println(*p2) // 55
var pp **int = &p1
println(**pp) // 5
pp = &p2
println(**pp) // 55
}
我们看到,**int
类型的变量 pp
中存储的是 *int
型变量的地址,这和前面的 *int
型变量存储的是 int
型变量的地址的情况,其实是一种原理。**int
被称为二级指针,也就是指向指针的指针,那自然,我们可以理解 *int
就是一级指针了。
对一级指针解引用,我们得到的其实是指针指向的变量。而对二级指针 pp
解引用一次,我们得到将是 pp
指向的指针变量:
println((*pp) == p1) // true
那么对 pp
解引用二次,我们将得到啥呢?对 pp
解引用两次,其实就相当于对一级指针解引用一次,我们得到的是 pp
指向的指针变量所指向的整型变量:
println((**pp) == (*p1)) // true
println((**pp) == a) // true
8. Go 指针作为函数参数
Go
语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。
ackage main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200
fmt.Printf("交换前 a 的值 : %d\n", a )
fmt.Printf("交换前 b 的值 : %d\n", b )
/* 调用函数用于交换值
* &a 指向 a 变量的地址
* &b 指向 b 变量的地址
*/
swap(&a, &b);
fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址的值 */
*x = *y /* 将 y 赋值给 x */
*y = temp /* 将 temp 赋值给 y */
}
指针无论是在 Go
中,还是在其他支持指针的编程语言中,存在的意义就是为了是“可改变”。在 Go
中,我们使用 *T
类型的变量调用方法、以 *T
类型作为函数或方法的形式参数、返回 *T
类型的返回值等的目的,也都是因为指针可以改变其指向的内存单元的值。
当然,指针的好处,还包括它传递的开销是常数级的(在 x86-64 平台上仅仅是 8 字节的拷贝),可控可预测。无论指针指向的是一个字节大小的变量,还是一个拥有 10000 个元素的 [10000]int 型数组,传递指针的开销都是一样的。