Golang指针、结构体及接口(六)
指针
一个指针可以指向任何一个值的内存地址,它指向那个值的内存地址,在32位机器上占用4个字节,在64位机器上占用8个字节,
并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性;
你可以在指针类型前面加上*号来获取指针所指向的内容,这里的*号是一个类型更改器。使用一个指针引用一个值被称为间接引用。
在Go语言中,直接砍掉了C语言指针最复杂的指针运算部分,只留下了获取指针(&运算符)和获取对象(*运算符)的运算,
用法和C语言很类似。
Go语言中一个指针被定义后没有分配到任何变量时,它的值为nil。
Go语言自带指针隐式解引用:对于一些复杂类型的指针,如果要访问成员变量时候需要写成类似*p.field的形式时,
只需要p.field即可访问相应的成员。
每个变量都是一个内存位置,每个内存位置都有其定义的地址,可以使用符号(&)运算符来访问它,
这个运算符表示内存中的地址。参考下面的例子,它将打印定义的变量的地址:
package main
import "fmt"
func main() {
var a int = 10
fmt.Printf("Address of a variable: %x\n", &a ) // Address of a variable: c420014080
}
指针是一个变量,其值是另一个变量的地址,即存储器位置的直接地址。类似变量或常量一样,必须要先声明一个指针,然后才能使用它来存储任何变量地址。
指针变量声明的一般形式是:
类型*T是指向类型T的值的指针。指针变量声明的一般形式是:
var var-name *var-type
这里,var-type是指针的基类型; 它必须是有效的Go数据类型,var-name是指针变量的名称。
用于声明指针的星号(*)与用于乘法的星号相同。但是,在此语句中,星号(*)用于将变量指定为指针。
package main
import "fmt"
func main() {
var ptr *int
fmt.Printf("The value of ptr is : %x\n", ptr )
}
当上面的代码编译和执行时,它产生结果是The value of ptr is 0,
在大多数操作系统上,程序不允许访问地址0处的内存,因为该内存是由操作系统保留的。然而存储器地址0具有特殊意义; 它表示指针不打算指向可访问的存储器位置。
但是按照惯例,如果指针包含nil(零)值,则假设它不指向任何东西。
要检查是否为nil指针,可以使用if语句,如下所示:
if(ptr != nil) /* succeeds if p is not nil */
if(ptr == nil) /* succeeds if p is null */
&符号会生成一个指向其作用对象的指针。
i := 42
p = &i
*符号表示指针指向的底层的值。
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i
这也就是通常所说的“间接引用”或“非直接引用”。
与C不同,Go没有指针运算。
package main
import "fmt"
func main() {
i, j := 42, 2701
p := &i // point to i
fmt.Println(*p) // read i through the pointer
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i
p = &j // point to j
*p = *p / 37 // divide j through the pointer
fmt.Println(j) // see the new value of j
}
new函数
使用new函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:
var t *T = new(T)
变量t是一个指向T的指针,此时结构体字段的值是它们所属类型的零值。
package main
import (
"fmt"
)
type Student struct {
name string
age int
weight float32
score []int
}
func main(){
//使用new关键字创建一个指针
pp := new(Student)
*pp = Student{"xiaoming", 23, 65.0, []int{2, 3, 6}}
fmt.Printf("stu pp have %d subjects\n", len((*pp).score))
fmt.Printf("stu pp have %d subjects\n", len(pp.score)) //Go语言自带隐式解引用
}
执行结果:
stu pp have 3 subjects
stu pp have 3 subjects
我们只需要使用new()函数,无需担心其内存的生命周期或怎样将其删除,因为Go语言的内存管理系统会帮我们打理一切。
func swap(p1, p2 *int) {
*p1, *p2 = *p2, *p1
}
func main() {
var a, b = 10, 20
swap(&a, &b)
fmt.Println("a = ", a, " b = ", b)
}
结构体
可以理解成是java中的类。
要定义结构体,必须使用type和struct语句。struct语句定义了一个新的数据类型,在程序中有多个成员。
type语句在例子中绑定一个类型为struct的名字。struct语句的格式如下:
type Student struct {
name string
age int
weight float32
score []int
}
要想调用结构体中的字段直接使用使用点号来访问。
package main
import "fmt"
type Vertex struct {
X int
Y int
}
func main() {
fmt.Println(Vertex{1, 2}) // {1 2}
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X) // 4
}
结构体字段可以通过结构体指针来访问。 通过指针间接的访问是透明的。
package main
import (
"fmt"
)
type Student struct {
name string
age int
weight float32
score []int
}
func main() {
//按照字段顺序进行初始化
stu01 := Student{"xiaomi", 23, 55.5, []int{95, 96, 98}}
//通过 field:value 形式初始化,该方式可以自定义初始化字段的顺序
stu02 := Student{age: 23, weight: 55.5, score: []int{97, 98}, name: "xiaohong"}
stu01.age = 25
fmt.Println(stu01.age)
fmt.Println(stu02)
}
结构体符文表示通过结构体字段的值作为列表来新分配一个结构体。
使用Name:语法可以仅列出部分字段。(字段名的顺序无关)。
特殊的前缀&返回一个指向结构体的指针。
package main
import "fmt"
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 类型为 Vertex
v2 = Vertex{X: 1} // Y:0 被省略
v3 = Vertex{} // X:0 和 Y:0
p = &Vertex{1, 2} // 类型为 *Vertex
)
func main() {
fmt.Println(v1, p, v2, v3) // {1 2} &{1 2} {1 0} {0 0}
}
- 通过指针操作结构体
var s Student
var p1 *Student
p1 = &s
// 通过指针操作成员 p1.id 和 (*p1).id完全等价
p1.id = 1
(*p1).name = "hello"
结构体new函数
一般在进行例如type T struct {a, b int}的结构体定义之后,习惯使用t := new(T)给该结构体变量分配内存,它返回指向已分配内存的指针。
变量t是一个指向T的指针,此时结构体字段的值是它们所属类型的零值。
声明var t T也会给t分配内存,并零值化内存,但是这个时候t是类型T。在这两种方式中,t通常被称做类型T的一个实例(instance)或对象(Object)。
一个简单的结构体new函数的例子如下:
package main
import "fmt"
type struct1 struct {
i1 int
f1 float32
str string
}
func main() {
ms := new(struct1) // ms是结构体struct1的指针
ms.i1 = 10
ms.f1 = 15.5
ms.str= "Chris"
fmt.Printf("The int is: %d\n", ms.i1)
fmt.Printf("The float is: %f\n", ms.f1)
fmt.Printf("The string is: %s\n", ms.str)
fmt.Println(ms)
}
执行结果:
The int is: 10
The float is: 15.500000
The string is: Chris
&{10 15.5 Chris}
结构体的比较
使用==比较和java中比较的内存地址不一样,这里==比较的是里面的元素是否相同
// 伪代码
s1 := Student{1, "hello"}
s2 := Student{1, "hello"}
s3 := Student{2, "hello"}
s1 == s2 // true
s1 == s3 // false
结构体作为参数
结构体作为参数时是值传递,形参无法改变实参数
func main() {
s := Student{1, "hello"}
test(s)
fmt.Println("main :", s)
}
func test(s Student) {
s.id = 666
fmt.Println("test 01 :", s)
}
执行结果:
test 01 : {666 hello}
main : {1 hello}
所以如果想要修改原数据,就必须通过引用传递的方式,也就是把指针传递过去 也就是说地址传递(引用传递),形参可以改实参数
func main() {
s := Student{1, "hello"}
test(&s)
fmt.Println("main :", s)
}
func test(s *Student) {
s.id = 666
fmt.Println("test 01 :", s)
}
执行结果:
test 01 : &{666 hello}
main : {666 hello}
匿名字段
结构体可以包含一个或多个匿名(或内嵌)字段,即这些字段没有显式的名字,只有字段的类型是必须的, 此时类型就是字段的名字。匿名字段本身可以是一个结构体类型,即 结构体可以包含内嵌结构体。
type Person struct {
name string
age int
}
type Student struct {
Person // 只有类型,没有名字就叫做匿名字段,继承了Person里面的成员
class string
byte // 性别 'm' 'f'
}
func main() {
s := Student{Person{"hello", 1}, "幼儿园", 'm'}
fmt.Println("s : ", s)
}
输出结果:
s : {{hello 1} 幼儿园, 109}
通过类型outer.int的名字来获取存储在匿名字段中的数据,于是可以得出一个结论:
在一个结构体中对于每一种数据类型只能有一个匿名字段。
内嵌结构体
同样地结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用,如同上面例子中那样。
外层结构体通过outer.in1直接进入内层结构体的字段,内嵌结构体甚至可以来自其他包。
在其他语言中,有继承可以做同样的事情,但是在Go语言中,没有继承的概念,Go提倡的代码复用的方式是组合,所以这也是嵌入类型的意义所在,组合而不是继承,所以Go才会更灵活。
内层结构体被简单的插入或者内嵌进外层结构体。这个简单的“继承”机制提供了一种方式,使得可以从另外一个或一些类型继承部分
或全部实现。
package main
import "fmt"
type A struct {
ax, ay int
}
type B struct {
A
bx, by float32
}
func main() {
b := B{A{1, 2}, 3.0, 4.0}
fmt.Println(b.ax, b.ay, b.bx, b.by)
fmt.Println(b.A)
}
执行结果:
1 2 3 4
{1 2}
同样这种嵌入类型也可以使用到接口中:
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
type ReadWriter interface {
Reader
Writer
}
type ReadCloser interface {
Reader
Closer
}
type WriteCloser interface {
Writer
Closer
}
以上是标准库io包里,我们常用的接口,可以看到ReadWriter接口是嵌入Reader和Reader接口而组合成的新接口,这样我们就不用重复的定义被嵌入接口里的方法,直接通过嵌入就可以了。
因为Go中没有像java中的private,public这种关键字,所以如果想要使用别的包中的函数、结构体类型、结构体成员,函数名、类型名、结构体成员变量名,首字母必须大写,才可以,如果首字母是小写,只能在同一个包中使用。
接口
Go编程提供了另一种称为接口(interfaces)的数据类型,它代表一组方法签名。
定义了一个接口之后,一般使用一个自定义结构体(struct)去实现接口中的方法。在Go语言中,任何实现了接口的函数的类型,都可以看作是接口的一个实现。
在Go中,如果你这个类型(基本类型,结构体等都可以)拥有了接口的所有方法,那么就默认为这个类型实现了这个接口,是隐式的,不需要和java一样,强制使用implement强制实现。
/* define an interface */
type interface_name interface {
method_name1 [return_type]
method_name2 [return_type]
method_name3 [return_type]
...
method_namen [return_type]
}
/* define a struct */
type struct_name struct {
/* variables */
}
/* implement interface methods*/
func (struct_name_variable struct_name) method_name1() [return_type] {
/* method implementation */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {
/* method implementation */
}
示例:
package main
import (
"fmt"
)
type Phone interface {
call()
}
type NokiaPhone struct{}
type IPhone struct{}
// NokiaPhone结构体实现Phone接口的call方法
func (nokiaPhone NokiaPhone) call() {
fmt.Println("I am Nokia, I can call you!")
}
// IPhone结构体实现Phone接口的call方法
func (iPhone IPhone) call() {
fmt.Println("I am iPhone, I can call you!")
}
func main() {
var phone Phone
phone = NokiaPhone{}
phone.call()
phone = IPhone{}
phone.call()
}
执行结果:
I am Nokia, I can call you!
I am iPhone, I can call you!
接口的继承
接口的继承也是通过匿名字段的方式实现的。
type Humaner interface {
sayHi()
}
type Personer interface {
Humaner // Person就继承了Humaner里面的所有方法
sing()
}
type Student struct {
name string
id int
}
func (stu Student) sayHi() {
fmt.Println("say hi")
}
func (stu Student) sing() {
fmt.Println("听天使在唱歌")
}
func main() {
var i Personer
s := Student{"hello", 1}
i = s
i.sayHi() // 继承过来的
i.sing()
}
空接口
空接口(interface{})不包含任何的方法,正因为如此,所有的类型都实现了空接口,因此空接口可以存储任意类型的数值。它有点类似于C语言的void *类型
var v1 interface{} = 1
var v2 interface{] = "abc"
var v3 interface{} = &v2
var v4 interface{} = struct{X int}{1}
// 我们的打印函数就是一个可变的空接口类型,因为它可以打印任意类型
func Println(args ...interface{})
- 邮箱 :charon.chui@gmail.com
- Good Luck!