程序本身是利用存放在机器内存中的数据,并执行机器指令的一系列过程。以两数相加为例,这两个数据必须存在机器内存当中。而存放这两个数据的那部分内存区域,需要首先询问机器进行内存的分配。在Go
语言中,可以轻松的对大部分类型通过初始化操作实现。
例如如下代码:
package main
import "fmt"
func main() {
i := 5
var j int
fmt.Println("i is: ", i)
fmt.Println("j is: ", j)
}
i is: 5
j is: 0
Go
会自动的为i
进行内存分配——分配一个整型大小空间的区域。在实例代码中,i := 5
说明在空间分配之后,数值5
便被塞到了那个空间中。对于变量j
,则没有分配任何值。然而,Go
会默认对大部分类型分配一个默认的zero-value
。对于数值变量而言,即为0
。
如下图,展示了内存现在的结构:
至此,i
的值为5
, j
的值为0
。
内置类型默认值
让我们举几个简短的例子,查看一下在Go
中已知类型的zero-value
或者说默认值。
代码如下:
package main
import "fmt"
func main() {
var i int
fmt.Println("default int is: ", i)
var s string
fmt.Println("default string is: ", s)
var f float64
fmt.Println("default float64 is: ", f)
var arInt [3]int
fmt.Println("default int array is: ", arInt)
var c complex64
fmt.Println("default complex64 is: ", c)
}
default int is: 0
default string is:
default float64 is: 0
default int array is: [0 0 0]
default complex64 is: (0+0i)
内存地址
每个内存变量都对应一个实际的物理地址。许多编程语言,包括Go
,都允许通过地址访问数据。
如下代码:
package main
import "fmt"
func main() {
i := 5
fmt.Println("i is: ", i)
fmt.Println("address of i is: ", &i)
}
i is: 5
address of i is: 0xf840000040
注意上面的代码,通过&
符号,可以获取一个变量的实际物理地址。
让我们再多举几个栗子:
package main
import "fmt"
func main() {
var i int
fmt.Println("address of i is: ", &i)
var s string
fmt.Println("address of s is: ", &s)
var f float64
fmt.Println("address of f is: ", &f)
var c complex64
fmt.Println("address of c is: ", &c)
}
address of i is: 0xf840000040
address of s is: 0xf8400013e0
address of f is: 0xf8400000f8
address of c is: 0xf8400000f0
实际的地址输出,根据机器的不同而不同,同样的代码两次执行的输出结果也不同。因为,机器之后的内存布局不同,另外在程序执行时,每次都是动态分配的地址空间。
所以,你可能要问:由于不同的机器地址空间变化,那么程序是不是也会产生不同的结果?地址确实会产生变化,但是大多数程序不是使用实际的地址数值进行工作的。它们实际上是用的地址中所包含的数据。可以通过在地址前面添加*
获取地址内部保存的数值。让我们列举一些实例。
package main
import "fmt"
func main() {
var i int
fmt.Println("value of i is: ", i)
fmt.Println("address of i is: ", &i)
fmt.Println("value at address ", &i, " is: ", *(&i)) //value at (address of i)
fmt.Println()
var s string
fmt.Println("value of s is: ", s)
fmt.Println("address of s is: ", &s)
fmt.Println("value at address ", &s, " is: ", *&s) value at address of i
fmt.Println()
var f float64
fmt.Println("value of f is: ", f)
fmt.Println("address of f is: ", &f)
fmt.Println("value at address ", &f, " is: ", *&f)
fmt.Println()
var c complex64
fmt.Println("value of c is: ", c)
ptr := &c //address of c.
fmt.Println("address of c is: ", ptr)
fmt.Println("value at address ", ptr, " is: ", *ptr) //value at the address
}
value of i is: 0
address of i is: 0xf840000040
value at address 0xf840000040 is: 0
value of s is:
address of s is: 0xf8400013b0
value at address 0xf8400013b0 is:
value of f is: 0
address of f is: 0xf8400000e8
value at address 0xf8400000e8 is: 0
value of c is: (0+0i)
address of c is: 0xf8400000b8
value at address 0xf8400000b8 is: (0+0i)
这种地址变量,成为指针。在上面的例子中 ptr := &c
, ptr
就是一个指针,它指向c
的地址空间,所以ptr
是变量c
的指针,可以认为ptr
是c
的一个引用。前面叙述的那些都能够正常运作,但是在使用时有些许区别。
如果,i := 5; ptr := &i
, 可以用下图粗劣的表示这种关系。使用时,i
和 *ptr
都表示整型数值5
。
指针必须指向一个变量,不能指向一个具体的数值或者静态变量,如下面的例子所示。
package main
func main() {
const i = 5
ptr := &i //error: cannot take the address of i
ptr2 := &10 //error: cannot take the address of 10
}
地址/指针/引用的用图
为什么需要地址/指针/引用这么多复杂的设计?直接使用真实数值不就很好么?
第一个原因是效率问题。在后面讨论传值和传引用时会讲述更多的内容。让我们打个比方。例如在维基百科上的一篇内容,比如巴黎:http://en.wikipedia.org/wiki/Paris。现在我们想把整个内容都发送给某人,一种方式是将整篇内容拷贝下来,通过邮件或者打印稿的方式发送给他。一种更加快速和方便的方式是仅仅把链接——唯一的URL,发过去即可。通过这种方式,没有额外的拷贝,同时你们两个都能通过链接读取到最新的内容。第一种发送整篇内容的方式,就如同传值——传输全部的值*,只发送链接,如同传引用,实际上传输的只是地址。
上述两个方式根据场景不同各自都有有用之处。当传递引用时,实际的数据只有一份,所有的改动都是对原数据的改动。传值,则会产生多份拷贝,各自更改并不会相互影响。
Golang一种神奇的语言,让我们一起进步