Go语言基础之结构体 Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
结构体 Go语言中的基础数据类型可以表示一些事物的基本属性,但是当想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。
Go语言中通过struct来实现面向对象。
结构体的定义 使用type和struct关键字来定义结构体,具体代码格式如下:
1 2 3 4 5 type 类型名 struct { 字段名 字段类型 字段名 字段类型 … }
其中:
类型名:标识自定义结构体的名称,在同一个包内不能重复。
字段名:表示结构体字段名。结构体中的字段名必须唯一。
字段类型:表示结构体字段的具体类型。
举个例子,我们定义一个Person(人)结构体,代码如下:
1 2 3 4 5 type person struct { name string city string age int8 }
同样类型的字段也可以写在一行
1 2 3 4 type person1 struct { name, city string age int }
这样我们就拥有了一个person的自定义类型,它有name、city、age三个字段,分别表示姓名、城市和年龄。这样我们使用这个person结构体就能够很方便的在程序中表示和存储人信息了。
语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型
实例化示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type info struct { name, addr string aget int } func main () { var Info info Info.name = "张三" Info.addr = "上海" Info.aget = 18 fmt.Println(Info) fmt.Printf("%#v\n" ,Info) fmt.Println(Info.name) }
我们通过.来访问结构体的字段(成员变量),例如Info.name 和 Info.age等。
取结构体的地址实例化 &{}
使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 type info struct { name, addr string aget int } func main () { Info := &info{} fmt.Printf("%T\n" , Info) fmt.Printf("%#v\n" , Info) Info.name = "zhangsan" Info.aget = 18 Info.addr = "上海" fmt.Printf("%#v\n" , Info) }
Info.name = "zhangsan"其实在底层是(*Info).name = "zhangsan",这是Go语言帮我们实现的语法糖
匿名结构体
定义匿名结构体时没有 type 关键字,与其他定义类型的变量一样,如果在函数外部需在结构体变量前加上 var 关键字,在函数内部可省略 var 关键字。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { info:= struct { name string add string age int }{name: "zhangsan" ,age: 18 } fmt.Printf("%T\n" , info) fmt.Printf("%#v\n" , info) fmt.Println(info.name) info.name = "张三" fmt.Printf("%#v\n" , info) }
结构体初始化 没有初始化的结构体,其成员变量都是对应其类型的零值。
1 2 3 4 5 6 7 8 9 10 11 type info struct { name, addr string aget int } func main () { var Info info fmt.Printf("%#v\n" , Info) }
通过位置参数初始化 初始化时对应结构体里面的位置传递元素,这种方式必须和声明里面的位置和数据类型一致。
1 2 3 4 5 6 7 8 9 10 11 12 type info struct { name, addr string aget int } func main () { var Info info Info = info{"zhangsan" ,"上海" ,18 } fmt.Println(Info) }
通过键值对初始化 使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 type info struct { name, addr string aget int } func main () { a1 := info{name: "zhangsan" , aget: 18 } fmt.Printf("%#v\n" , a1) a2 := info{ name: "zhangsan" , addr: "上海" , aget: 18 , } fmt.Printf("%#v\n" , a2) }
也可以对结构体指针进行键值对初始化,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 type info struct { name, addr string aget int } func main () { a3 := &info{ name: "zhangsan" , addr: "上海" , aget: 18 , } fmt.Printf("%#v\n" , a3) }
初始化字段类型是切片或map时 需要注意的是:当定义结构体的字段类型是 []切片 或者是map[string]string时,结构体初始化时的字段需要make后再进行初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 package mainimport ( "fmt" ) type Person struct { Name string Age int Number []int Hot map [string ]string } func main () { var p Person p.Number = make ([]int , 1 ) p.Hot = make (map [string ]string ) p.Number[0 ] = 100 p.Hot["name" ] = "张三" fmt.Println(p) var p21 Person = Person{} p21.Name = "赵二一" fmt.Println(p21) var p2 *Person = &Person{} p2.Name = "赵六" fmt.Println(p2) var p3 = &Person{} p3.Name = "王七" fmt.Println(p3) p4 := Person{ Name: "李四" , Age: 18 , } fmt.Println(p4) p5 := &Person{} p5.Name = "王五" fmt.Println(p5) }
值传递和指针传递 在开始前你需要先掌握结构体的定义、声明以及使用方法;
值传递 我们先来看一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package maintype Info struct { Age int Name string } func change (t Info) { t.Age = 999 } func main () { info := Info{Age: 18 ,Name: "zhangsan" } change(info) println (info.Age) }
程序的输出是 18
因为这种方法传递的是值一个副本,在change() 函数中,你实际上修改的是副本的值;
指针传递 修改程序,让它使用指针进行传递:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package maintype Info struct { Age int Name string } func change (t *Info) { t.Age = 999 } func main () { info := Info{Age: 18 ,Name: "zhangsan" } change(&info) println (info.Age) }
这段程序使用了&取地址操作符来获取结构体的地址,而change()函数期望一个Info结构体的地址类型 *Info,这里*Info的意思是指向类型info值得指针;
程序运行输出 999
但我们需要注意的是,实际上这里传递的依然是一个副本,只不过这个副本是一个地址,它指向原来的值;
所以,你可以修改 info.Age的值,但你无法修改 Info;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package maintype Info struct { Age int Name string } func change (t *Info) { t = &Info{Age: 20 } } func main () { info := Info{Age: 18 ,Name: "zhangsan" } change(&info) println (info.Age) }
这段代码输出18
那么,应该如何选择传递方式呢?
很明显,复制一个指针比复制一个结构的消耗要小的多;如果我们的结构非常复杂和庞大,那么复制结构会是一个很消耗性能的操作,在进行大量这样的操作时你的感觉会非常明显;
而使用指针则可避免这个问题;
当你的函数本意是改变原始数据时,那么肯定用指针转递;
当你的结构非常大时,比如包含庞大的切片、map时,也需要用指针转递;
但是如果你的结构体非常小,且不打算修改结构体内容,那么应该考虑使用值传递;
因为你不能保证程序没有bug导致误修改;且在某些多任务环境下,你需要为指针操作添加额外的锁操作,这样有些得不偿失。
结构体方法
结构体方法是一种特殊的函数,它规定只有指定的接收者才能调用该方法,有点像面向对象编程语言中与类绑定的方法,接收者声明在func关键字之后,函数名之前。
方法的接收者分为值接收者与指针接收者,若方法内部有需要改变接收者的值,需要使用指针接收者。
值接收者是实参的值拷贝,会额外消耗内存空间,当接收者占用内存空间很大时,使用值接收者显然有点浪费空间,因此应尽量使用指针接收者。
指针对象调用值接收者的方法时,会自动解引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 type Person struct { Name string Age int } func (p *Person) Test() string { fmt.Println("Test方法内的打印:" , p.Name) return p.Name } func main () { p := &Person{Name: "张三" } name := p.Test() fmt.Println("Test方法返回:" , name) }
方法和函数的区别
调用方式不一样:函数的调用方式为 函数名(实参列表) 。方法的调用方式:变量.方法名(实参列表)
对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。
对于方法,如(struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。