广告
返回顶部
首页 > 资讯 > 后端开发 > GO >深入了解Golang的指针用法
  • 920
分享到

深入了解Golang的指针用法

2024-04-02 19:04:59 920人浏览 薄情痞子
摘要

目录1.指针类型的变量2.Go只有值传递,没有引用传递3.for range与指针4.闭包与指针5.指针与内存逃逸与C语言一样,Go语言中同样有指针,通过指针,我们可以只传递变量的内

C语言一样,Go语言中同样有指针,通过指针,我们可以只传递变量的内存地址,而不是传递整个变量,这在一定程度上可以节省内存的占用,但凡事有利有弊,Go指针在使用也有一些注意点,稍不留神就会踩坑,下面就让我们一起来细嗦下。

1.指针类型的变量

golang中,我们可以通过**取地址符号&**得到变量的地址,而这个新的变量就是一个指针类型的变量,指针变量与普通变量的区别在于,它存的是内存地址,而不是实际的值。

图一

如果是普通类型的指针变量(比如 int),是无法直接对其赋值的,必须通过 * 取值符号才行。

func main() {
	num := 1
	numP := &num
	
	//numP = 2 // 报错:(type untyped int) cannot be represented by the type *int
	*numP = 2
}

但结构体却比较特殊,在日常开发中,我们经常看到一个结构体指针的内部变量仍然可以被赋值,比如下面这个例子,这是为什么呢?

type Test struct {
	Num int
}

// 直接赋值和指针赋值
func main() {
	test := Test{Num: 1}
	test.Num = 3
	fmt.Println("v1", test) // 3

	testP := &test
	testP.Num = 4           // 结构体指针可以赋值
	fmt.Println("v2", test) // 4
}

这是因为结构体本身是一个连续的内存,通过 testP.Num ,本质上拿到的是一个普通变量,并不是一个指针变量,所以可以直接赋值。

图二

那slice、map、channel这些又该怎么理解呢?为什么不用取地址符号也能打印它们的地址?比如下面的例子

func main() {
	nums := []int{1, 2, 3}
	fmt.Printf("%p\n", nums)     // 0xc0000160c0
	fmt.Printf("%p\n", &nums[0]) // 0xc0000160c0

	maps := map[string]string{"aa": "bb"}
	fmt.Printf("%p\n", maps) // 0xc000076180

	ch := make(chan int, 0)
	fmt.Printf("%p\n", ch) // 0xc00006c060
}

这是因为,它们本身就是指针类型!只不过Go内部为了书写的方便,并没有要求我们在前面加上 *** 符号**。

在Golang的运行时内部,创建slice的时候其实返回的就是一个指针:

// 源码  runtime/slice.go
// 返回值是:unsafe.Pointer
func makeslice(et *_type, len, cap int) unsafe.Pointer {
	mem, overflow := math.MulUintptr(et.size, uintptr(cap))
	if overflow || mem > maxAlloc || len < 0 || len > cap {
		// NOTE: Produce a 'len out of range' error instead of a
		// 'cap out of range' error when someone does make([]T, bignumber).
		// 'cap out of range' is true too, but since the cap is only being
		// supplied implicitly, saying len is clearer.
		// See golang.org/issue/4085.
		mem, overflow := math.MulUintptr(et.size, uintptr(len))
		if overflow || mem > maxAlloc || len < 0 {
			panicmakeslicelen()
		}
		panicmakeslicecap()
	}

	return mallocGC(mem, et, true)
}

而且返回的指针地址其实就是slice第一个元素的地址(上面的例子也体现了),当然如果slice是一个nil,则返回的是 0x0 的地址。slice在参数传递的时候其实拷贝的指针的地址,底层数据是共用的,所以对其修改也会影响到函数外的slice,在下面也会讲到。

map和slice其实也是类似的,在在Golang的运行时内部,创建map的时候其实返回的就是一个hchan指针:

// 源码  runtime/chan.go
// 返回值是:*hchan
func makechan(t *chantype, size int) *hchan {
	elem := t.elem

	// compiler checks this but be safe.
	if elem.size >= 1<<16 {
		throw("makechan: invalid channel element type")
	}
	...
	return c
}

最后,为什么 fmt.Printf 函数能够直接打印slice、map的地址,除了上面的原因,还有一个原因是其内部也做了特殊处理:

// 第一层源码
func Printf(fORMat string, a ...interface{}) (n int, err error) {
	return Fprintf(os.Stdout, format, a...)
}

// 第二层源码
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintf(format, a)  // 核心
	n, err = w.Write(p.buf)
	p.free()
	return
}

// 第三层源码
func (p *pp) doPrintf(format string, a []interface{}) {
	 ...
	default:
			// Fast path for common case of ascii lower case simple verbs
			// without precision or width or argument indices.
			if 'a' <= c && c <= 'z' && argNum < len(a) {
				...
				p.printArg(a[argNum], rune(c))   // 核心是这里
				argNum++
				i++
				continue formatLoop
			}
			// Format is more complex than simple flags and a verb or is malformed.
			break simpleFormat
		}

}

// 第四层源码
func (p *pp) printArg(arg interface{}, verb rune) {
	p.arg = arg
	p.value = reflect.Value{}
  ...
	case 'p':
		p.fmtPointer(reflect.ValueOf(arg), 'p')
		return
	}
	...
}

// 最后了
func (p *pp) fmtPointer(value reflect.Value, verb rune) {
	var u uintptr
	switch value.Kind() {
  // 这里对这些特殊类型直接获取了其地址
	case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:
		u = value.Pointer()
	default:
		p.badVerb(verb)
		return
	}
  ...
}

2.Go只有值传递,没有引用传递

值传递和引用传递相信大家都比较了解,在函数的调用过程中,如果是值传递,则在传递过程中,其实就是将参数的值复制一份传递到函数中,如果在函数内对其修改,并不会影响函数外面的参数值,而引用传递则相反。

type User struct {
	Name string
	Age  int
}

// 引用传递
func setNameV1(user *User) {
	user.Name = "test_v1"
}

// 值传递
func setNameV2(user User) {
	user.Name = "test_v2"
}

func main() {
	u := User{Name: "init"}
	fmt.Println("init", u)  // init {init 0}

	up := &u
	setNameV1(up)
	fmt.Println("v1", u) // v1 {test_v1 0}

	setNameV2(u)
	fmt.Println("v2", u) // v2 {test_v1 0}
}

但在Golang中,这所谓的“引用传递”其实本质上是值传递,因为这时候也发生了拷贝,只不过这时拷贝的是指针,而不是变量的值,所以**“Golang的引用传递其实是引用的拷贝”。**

图三

可以通过以下代码验证:

type User struct {
	Name string
	Age  int
}

// 注意这里有个误区,我一开始看 user(v1)打印后的地址和一开始(init)是一致的,从而以为这是引用传递
// 其实这里的user应该看做一个指针变量,我们需要对比的是它的地址,所以还要再取一次地址
func setNameV1(user *User) {
	fmt.Printf("v1: %p\n", user)  // 0xc0000a4018  与 init的地址一致
	fmt.Printf("v1_p: %p\n", &user) // 0xc0000ac020
	user.Name = "test_v1"
}

// 值传递
func setNameV2(user User) {
	fmt.Printf("v2_p: %p\n", &user) //0xc0000a4030
	user.Name = "test_v2"
}

func main() {
	u := User{Name: "init"}

	up := &u
	fmt.Printf("init: %p \n", up) //0xc0000a4018
	setNameV1(up)
	setNameV2(u)
}

注:slice、map等本质也是如此。

3.for range与指针

for range是在Golang中用于遍历元素,当它与指针结合时,稍不留神就会踩坑,这里有一段经典代码:

type User struct {
	Name string
	Age  int
}

func main() {
	userList := []User {
		User{Name: "aa", Age: 1},
		User{Name: "bb", Age: 1},
	}

	var newUser []*User
	for _, u := range userList {
		newUser = append(newUser, &u)
	}

	// 第一次:bb
	// 第二次:bb
	for _, nu := range newUser {
		fmt.Printf("%+v", nu.Name)
	}
}

按照正常的理解,应该第一次输出aa,第二次输出bb,但实际上两次都输出了bb,这是因为 for range 的时候,变量u实际上只初始化了一次(每次遍历的时候u都会被重新赋值,但是地址不变),导致每次append的时候,添加的都是同一个内存地址,所以最终指向的都是最后一个值bb。

我们可以通过打印指针地址来验证:

func main() {
	userList := []User {
		User{Name: "aa", Age: 1},
		User{Name: "bb", Age: 1},
	}

	var newUser []*User
	for _, u := range userList {
		fmt.Printf("point: %p\n", &u)
		fmt.Printf("val: %s\n", u.Name)
		newUser = append(newUser, &u)
	}
}

// 最终输出结果如下:
point: 0xc00000c030
val: aa
point: 0xc00000c030
val: bb

类似的错误在Goroutine也经常发生:

// 这里要注意下,理论上这里都应该输出10的,但有可能出现执行到7或者其他值的时候就输出了,所以实际上这里不完全都输出10
func main() {
	for i := 0; i < 10; i++ {
		go func(idx *int) {
			fmt.Println("go: ", *idx)
		}(&i)
	}
	time.Sleep(5 * time.Second)
}

4.闭包与指针

什么是闭包,一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域

当闭包与指针进行结合时,如果闭包里面是一个指针变量,则外部变量的改变,也会影响到该闭包,起到意想不到的效果,让我们继续在举几个例子进行说明:

func incr1(x *int) func() {
	return func() {
		*x = *x + 1   // 这里是一个指针
		fmt.Printf("incr point x = %d\n", *x)
	}
}
func incr2(x int) func() {
	return func() {
		x = x + 1
		fmt.Printf("incr normal x = %d\n", x)
	}
}

func main() {
	x := 1
	i1 := incr1(&x)
	i2 := incr2(x)
	i1() // point x = 2
	i2() // normal x = 2
	i1() // point x = 3
	i2() // normal x = 3

	x = 100
	i1() // point x = 101  // 闭包1的指针变量受外部影响,被重置为100,并继续递增
	i2() // normal x = 4
	i1() // point x = 102
	i2() // normal x = 5
}

5.指针与内存逃逸

内存逃逸的场景有很多,这里只讨论由指针引发的内存逃逸。理想情况下,肯定是尽量减少内存逃逸,因为这意味着GC(垃圾回收)的压力会减小,程序也会运行得更快。不过,使用指针又能减少内存的占用,所以这本质是内存和GC的权衡,需要合理使用。

下面是指针引发的内存逃逸的三种场景(欢迎大家补充~)

第一种场景:函数返回局部变量的指针

type Escape struct {
	Num1  int
	Str1  *string
	Slice []int
}

// 返回局部变量的指针
func NewEscape() *Escape {
	return &Escape{}   // &Escape{} escapes to heap
}

func main() {
	e := &Escape{Num1: 0}
}

第二种场景:被已经逃逸的变量引用的指针

func main() {
	e := NewEscape()
	e.SetNum1(10)

	name := "aa"
	// e.Str1 中,e是已经逃逸的变量, &name是被引用的指针
	e.Str1 = &name  // moved to heap: name
}

第三种场景:被指针类型的slice、map和chan引用的指针

func main() {
	e := NewEscape()
	e.SetNum1(10)

	name := "aa"
	e.Str1 = &name

	// 指针类型的slice
	arr := make([]*int, 2) 
	n := 10  // moved to heap: n
	arr[0] = &n // 被引用的指针
}

以上就是深入了解Golang的指针用法的详细内容,更多关于Golang指针的资料请关注编程网其它相关文章!

您可能感兴趣的文档:

--结束END--

本文标题: 深入了解Golang的指针用法

本文链接: https://www.lsjlt.com/news/153917.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • 深入了解Golang的指针用法
    目录1.指针类型的变量2.Go只有值传递,没有引用传递3.for range与指针4.闭包与指针5.指针与内存逃逸与C语言一样,Go语言中同样有指针,通过指针,我们可以只传递变量的内...
    99+
    2022-11-13
  • 深入了解C语言指针
    目录指针是什么?指针和指针类型指针运算总结指针是什么? 总结:指针就是个变量,变量里面是地址,指针就是地址。(存放在指针的值都被当成地址处理)。 注:指针的大小在32位平台是4个字节...
    99+
    2022-11-12
  • 深入了解C++智能指针的使用
    目录一、C++11智能指针概述二、C++98中的智能指针三、C++11中的智能指针1.unique_ptr2.shared_ptr3.weak_ptr一、C++11智能指针概述 在C...
    99+
    2022-11-13
  • 深入理解c++指针的指针和指针的引用
    在C++中,指针是一种数据类型,可以存储变量的内存地址。指针的指针和指针的引用是两种特殊的指针类型。指针的指针是指一个指针变量的值是另一个指针的地址。也就是说,指针的指针存储的是一个指针变量的地址。可以通过多次间接引用来获取最终指向的变...
    99+
    2023-08-09
    c++
  • 深入了解C语言中的const和指针
    目录前言指针的赋值问题ANSI C 有关简单赋值的标准问题解决const修饰const修饰变量const修饰指针前言 文章内容由阅读《C专家编程》整理而来。希望可以帮助大家解决在指针...
    99+
    2022-11-13
  • 深入了解C++中常用的三个智能指针
    目录什么是智能指针C++中的几种智能指针unique_ptrshared_ptrweak_ptr总结什么是智能指针 在C++中,内存的分配和释放都是由开发者手动实现的。这种方式虽然很...
    99+
    2023-05-18
    C++智能指针 内存管理 C++智能指针使用 C++ 智能指针
  • 深入了解Golang中的run方法
    Go是一种快速,可靠和开源的编程语言。Go语言通过其高效的并发性和垃圾回收器以及C的速度,用于构建高效和可扩展的网络服务器和系统编程。让我们深入了解Golang中的run方法。run()方法是golang中重要的一种方法,可以用于创建新的协...
    99+
    2023-05-14
  • 带你了解C++this指针的用法及其深究
    目录前言一、this指针是个什么东东,重要吗?二、案例理解主要的用途总结前言 今天,码神像一个新车手一样,尝试着用模板来更新一下,不要建议哦,毕竟没有放弃爱情的拓海也不是真正的车神,...
    99+
    2022-11-12
  • C/C++中指针的深入理解
    目录计算机的内存模型指针与指针常量指针变量和指针常量指针变量和数组函数指针C++中的引用传值还是传引用C++中的new关键词总结计算机的内存模型 CPU是计算机的核心部件,要想让一...
    99+
    2022-11-12
  • 深入理解C语言的指针
    目录起源进程内存布局栈设置分配方式特点堆分配方式特点堆与栈区别扩展总结起源 之前在知乎上看了一句话,指针是C的精髓,也是初学者的一个坎。换句话说,内存管理是C的精髓,C/C++可以直...
    99+
    2022-11-12
  • 深入了解Golang官方container/heap用法
    目录开篇container/heap核心函数InitPop/PushRemoveFix如何接入IntHeap优先队列按时间戳排序总结开篇 在 Golang 的标准库 containe...
    99+
    2022-11-11
  • C++深入浅出讲解隐藏this指针的用法
    目录1.this指针的引出2.this指针的特性3.练习一下本篇文章我们将一起讨论在有趣的知识点--隐藏的this指针。本篇我们要使用到之前我们所学习到的C++类与对象,如果有各位小...
    99+
    2022-11-13
  • 深入了解golang中的静态方法
    Golang是一门支持面向对象编程的编程语言,但是它的面向对象编程方式有些不同于传统的面向对象编程语言。有一点不同是Golang中没有类(class)的概念,也没有Java中的静态方法(static method)。但是Golang中提供了...
    99+
    2023-05-14
  • 深入了解Golang包的获取方法
    目录1.获取远程包2.应用1.获取远程包 go 语言有一个获取远程包的工具就是 go get,目前 go get 支持多数开源社区 (例如:github、googlecode、bit...
    99+
    2022-11-13
  • 怎样深入理解C语言的指针
    本篇文章给大家分享的是有关怎样深入理解C语言的指针,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。起源之前在知乎上看了一句话,指针是C的精髓,也是初学者的一个坎。换句话说,内存管...
    99+
    2023-06-26
  • C语言深入浅出讲解指针的使用
    目录一、利用指针倒序字符串二、题目实例三、总结一、利用指针倒序字符串 void _reversal(char* left, char* right) { while (lef...
    99+
    2022-11-13
  • Golang中指针的使用详解
    目录1.概念2.指针定义与空指针3.指针赋值与取值4.切片指针5.指针的指针1.概念 指针:指向内存地址的变量,指针用来存储变量的内存地址 Go 语言定义变量必须声明数据类型,因为不...
    99+
    2023-05-18
    Golang指针 Go指针类型
  • 深入了解Golang中Slice切片的使用
    目录写在前面上代码分析原因总结写在前面 周日下午在家学习,看到一个关于切片的问题,在网上找了一些资料,做个总结。 上代码 func main() { sl := make([]in...
    99+
    2023-02-27
    Golang Slice切片使用 Golang Slice切片 Golang Slice
  • 深入了解Golang中占位符的使用
    目录基本常见常用的占位符较少使用的占位符进制和浮点使用占位符指针占位符基本常见常用的占位符 %s%d%v , %v+ , %+v%T , %q 写一个 demo 来看看上面占位符的效...
    99+
    2023-03-11
    Golang占位符使用 Golang占位符 Go 占位符
  • 深入了解Golang中reflect反射的使用
    目录1. 介绍2. 方法示例2.1 通过反射获取对象的键(类型)和值2.2 反射对象的类型和属性3. 反射对Json的操作3.1 反射与Json属性解析3.2 Json包的序列化与反...
    99+
    2023-05-20
    Golang reflect反射 Go reflect反射 Golang reflect
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作