Go结构体、方法、接口
1 结构体
Go语言中没有“类”的概念,也不支持像继承这种面向对象的概念。但是Go语言的结构体与“类”都是复合结构体,而且Go语言中结构体的组合方式比面向对象具有更高的扩展性和灵活性。
1.1 结构体定义
结构体一般定义如下:
type identifier struct {
field1 type1
field2 type2
...
}
例如我们想声明一个学生的结构体类型:
type Student struct {
Name string
Age int
}
结构体中字段的类型可以是任何类型,包括函数类型,接口类型,甚至结构体类型本身。例如我们声明一个链表中的节点的结构体类型。
type ListNode struct {
Val int
Next *ListNode
}
在声明结构体时我们也可以不给字段指定名字,例如下面这样
type Person struct{
ID string
int
}
我们可以看到其中有一个int字段没有名字,这种我们称其为匿名字段。
1.2 操作结构体
声明完结构体之后我们需要创建结构体的实例,可以使用如下几种方法创建,仍然以上面的Student结构体为例。
- 使用new函数会创建一个指向结构体类型的指针,创建过程中会自动为结构体分配内存,结构体中每个变量被赋予对应的零值。
- 也可以使用第二种方式声明结构类型,需要注意的是此时给结构体赋值的顺序需要与结构体字段声明的顺序一致。
- 第三种方式更为常用,我们创建结构体的同时显示的为结构体中每个字段进行赋值。
声明完结构体之后可以直接按如下方式操作结构体。
package main
import "fmt"
type Student struct {
Name string
Age int
int
}
func main() {
s1 := new(Student) //第一种方式
s2 := Student{"james", 35, 100} //第二种方式
s3 := &Student{ //第三种方式
Name: "LeBron",
Age: 36,
}
//直接打印结构体
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(s3)
//直接操作结构
s1.Name = "jack"
s1.Age = 35
//匿名字段
s1.int = 85
fmt.Println(s1)
}
执行结果
&{ 0 0}
{james 35 100}
&{LeBron 36 0}
&{jack 35 85}
我们直接通过s1.int的方式来访问结构体中的匿名字段对其赋值,通过这个例子也可以发现,对于一个结构体来说,每一种数据类型只能有一个匿名字段。
1.3 标签
在go语言中结构体除了字段的名称和类型还有一个可选的标签tag,标记的tag只有reflect包可以访问到,一般用于orm或者json的数据传递。
另外,我们可以使用go自带的json包将声明的结构体变量转变为json字符串。
如果我们没有给结构体打标签输出的json字符串:
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string
Age int
int
}
type Student1 struct {
Name string `json:"name"`
Age int `json:"age"`
int `json:score`
}
func ToJson(s *Student) (string, error) {
bytes, err := json.Marshal(s)
if err != nil {
return "", nil
}
return string(bytes), nil
}
func ToJson1(s *Student1) (string, error) {
bytes, err := json.Marshal(s)
if err != nil {
return "", nil
}
return string(bytes), nil
}
func main() {
//如果我们没有给结构体打标签的话
s1 := new(Student)
s1.Name = "jack"
s1.Age = 28
s1.int = 100
str, _ := ToJson(s1)
fmt.Println("如果我们没有给结构体打标签的话")
fmt.Println(str)
fmt.Println()
s2 := new(Student1)
s2.Name = "jack"
s2.Age = 28
s2.int = 100
str1, _ := ToJson1(s2)
fmt.Println("如果我们给结构体打过标签之后的话")
fmt.Println(str1)
}
执行结果
如果我们没有给结构体打标签的话
{"Name":"jack","Age":28}
如果我们给结构体打过标签之后的话
{"name":"jack","age":28}
1.4 内嵌结构体
之前我们介绍到了匿名字段,结构体作为一种数据类型也可以将其声明为匿名字段,此时我们称其为内嵌结构体,下面这段代码中我们将结构体A嵌入到结构体B中。通过内嵌结构体的方式我们可以在结构体B的变量下很方便地操作A中定义的字段。
package main
import "fmt"
//定义A结构体
type A struct {
X, Y int
}
//定义B结构体,内嵌A结构体
type B struct {
A
Name string
}
func main() {
//操作内嵌了结构体的结构体
b := new(B)
b.X = 10
b.Y = 20
b.Name = "jack"
fmt.Println(b)
}
执行结果
&{{10 20} jack}
可以看到在b中操作结构体A中定义的字段就像结构体B本身定义的字段一样自然。
但是如果存在字段的名称冲突我们该怎么办?假如结构体C中也有字段X,内嵌的结构体A中也有字段X,让我们尝试一下如何,能不能成功运行?
package main
import "fmt"
//定义A结构体
type A struct {
X, Y int
}
//定义B结构体,内嵌A结构体
type B struct {
A
Name string
}
//定义C结构体,有冲突的X字段
type C struct {
A
B
X int
}
func main() {
//有字段冲突的结构体
c := new(C)
c.X = 10 //关注字段
c.Y = 20
c.Name = "jack"
fmt.Println(c)
//此事可看出只是赋值给我C下的X,如果要赋值给A和B的X呢?
c.A.X = 11
c.A.Y = 22
c.B.X = 111
c.B.Y = 222
c.B.Name = "rose"
fmt.Println(c)
}
执行结果
&{{0 20} {{0 0} jack} 10}
&{{11 22} {{111 222} rose} 10}
需要注意的是,内嵌结构体和声明一个结构体类型的字段是不同的
可以尝试一下在结构体中定义一些复杂类型例如切片
package main
import "fmt"
//定义A结构体
type A struct {
X, Y int
Z []int
}
func main() {
v := new(A)
v.X = 1
v.Y = 2
v.Z = make([]int, 10)
fmt.Println(v)
v.Z[0] = 11
v.Z[1] = 21
v.Z[2] = 31
v.Z[3] = 41
v.Z[4] = 51
fmt.Println(v)
}
执行结果
&{1 2 [0 0 0 0 0 0 0 0 0 0]}
&{1 2 [11 21 31 41 51 0 0 0 0 0]}
2 方法
2.1 方法定义
方法与函数类似,只不过在方法定义时会在func和方法名之间增加一个参数,如下所示:
func (r Receiver)func_name(){
//body
}
其中r被称为方法的接收者
package main
import "fmt"
type Person struct {
name string
}
func (p Person) GetName() string {
return p.name
}
func main() {
p := Person{
name: "james",
}
fmt.Println(p.GetName())
}
其中GetName方法的接收者为p是Person结构体类型,也就是说我们为结构体Person绑定了一个GetName方法,我们可以使用如上的方式进行调用。
2.2 方法接收者
对于一个方法来说接收者分为两种类型:值接收者和指针接收者。上面的GetName的接收者就是值接收者。我们再为Person结构体定义一个指针接收者。
使用值接收者定义的方法,在调用时使用的其实是值接收者的一个拷贝,所以对该值的任何操作,都不会影响原来的类型变量。
但是如果使用指针接收者的话,在方法体内的修改就会影响原来的变量,因为指针传递的也是地址,但是是指针本身的地址,此时拷贝得到的指针还是指向原值的,所以对指针接收者操作的同时也会影响原来类型变量的值。
package main
import "fmt"
type Person struct {
name string
}
func (p Person) GetName() string {
return p.name
}
func (p Person) SetName(name string) {
p.name = name
}
func (p *Person) SetNameWithRef(name string) {
p.name = name
}
func main() {
p := Person{
name: "james",
}
p.SetName("jack")
fmt.Println("值传递为jack", p.GetName())
p.SetNameWithRef("jack")
fmt.Println("指针传递为jack", p.GetName())
}
执行结果
值传递为jack james
指针传递为jack jack
而且在go语言中还有一点比较特殊,我们使用值接收者定义的方法使用指针来调用也是可以的,反过来也是如此,如下所示:
package main
import "fmt"
type Person struct {
name string
}
func (p Person) GetName() string {
return p.name
}
func (p Person) SetName(name string) {
p.name = name
}
func (p *Person) SetNameWithRef(name string) {
p.name = name
}
func main() {
p := &Person{
name: "james",
}
p.SetName("kobe")
fmt.Println(p.GetName())
p1 := Person{
name: "james",
}
p1.SetName("kobe")
fmt.Println(p1.GetName())
p2 := &Person{
name: "james",
}
p2.SetNameWithRef("kobe")
fmt.Println(p2.GetName())
p3 := Person{
name: "james",
}
p3.SetNameWithRef("kobe")
fmt.Println(p3.GetName())
}
执行结果
james
james
kobe
kobe
3 接口
3.1 接口定义
接口相当于一种规范,它需要做的是谁想要实现我这个接口要做哪些内容,而不是怎么做。在go语言中接口的定义如下所示:
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
3.2 实现接口
在go语言中不需要显示的去实现接口,只要一个类型实现了该接口中定义的所有方法就是默认实现了该接口,而且允许多个类型都实现该接口,也允许一个类型实现多个接口。
package main
import "fmt"
//定义一个动物接口
type Animal interface {
Eat()
}
//再定义鸟结构体
type Bird struct {
Name string
}
//实现吃接口
func (b Bird) Eat() {
fmt.Println(b.Name + "吃虫")
}
//再定义狗结构体
type Dog struct {
Name string
}
//实现吃接口
func (d Dog) Eat() {
fmt.Println(d.Name + "吃肉")
}
func EatWhat(a Animal) {
a.Eat()
}
func main() {
b := Bird{"鸟"}
d := Dog{"狗"}
EatWhat(b)
EatWhat(d)
}
执行结果
鸟吃虫
狗吃肉
在EatWhat函数中是传递一个Animal接口类型,上面的Bird和Dog结构体都实现了Animal接口,所以都可以传递到函数中去来实现多态特性。
但是还有几点需要大家去探索一下:
- 通过值接收者和指针接收者定义的方法,对于接口的实现有什么影响吗?
package main
import "fmt"
//定义一个动物接口
type Animal interface {
Eat()
}
//再定义鸟结构体
type Bird struct {
Name string
}
//实现吃接口
func (b *Bird) Eat() {
fmt.Println(b.Name + "吃虫")
}
func EatWhat(a Animal) {
a.Eat()
}
func main() {
b := Bird{"鸟"}
EatWhat(&b)
}
从上述结果来看,值接收者与指针接收者的区别在于加不加*&组合
- 还记得我们之前说过的内嵌结构体么,如果嵌入的结构体实现了某个接口那么对于外部的结构体有什么影响吗?
package main
import "fmt"
//定义一个动物接口
type Animal interface {
Eat()
}
//再定义鸟结构体
type Bird struct {
Name string
}
//实现吃接口
func (b Bird) Eat() {
fmt.Println(b.Name + "吃虫")
}
//再定义狗结构体
type Dog struct {
Name string
Bird
}
//实现吃接口
// func (d Dog) Eat() {
// fmt.Println(d.Name + "吃肉")
// }
func EatWhat(a Animal) {
a.Eat()
}
func main() {
b := Bird{"鸟"}
d := &Dog{"狗", b}
EatWhat(b)
EatWhat(d)
}
执行结果
鸟吃虫
鸟吃虫
如果,把func (d Dog) Eat() {注释放开的话
鸟吃虫
狗吃肉
3.3 类型断言
有些时候方法传递进来的参数可能是一个接口类型,但是我们要继续判断是哪个具体的类型才能进行下一步操作,这时就用到了类型断言,下面我们通过一个例子来进行讲解:
func IsDog(a Animal) bool {
if v, ok := a.(Dog); ok {
fmt.Println(v)
return true
}
return false
}
上面的方法对传递进来的参数进行判断,判断其是否为Dog类型,如果是Dog类型的话就会将其进行转换为v,ok用来表示是否断言成功。
但是如果我们对于一个类型有好多种子类型要进行判断,这样写的话显然是有些复杂,可以使用如下这种方式:
func WhatType(a Animal) {
switch a.(type) {
case Dog:
fmt.Println("Dog")
case Bird:
fmt.Println("Bird")
default:
fmt.Println("error")
}
}
整段代码
package main
import "fmt"
//定义一个动物接口
type Animal interface {
Eat()
}
//再定义鸟结构体
type Bird struct {
Name string
}
//实现吃接口
func (b Bird) Eat() {
fmt.Println(b.Name + "吃虫")
}
//再定义狗结构体
type Dog struct {
Name string
}
//实现吃接口
func (d Dog) Eat() {
fmt.Println(d.Name + "吃肉")
}
func IsDog(a Animal) bool {
if v, ok := a.(Dog); ok {
fmt.Println(v)
return true
}
return false
}
func WhatType(a Animal) {
switch a.(type) {
case Dog:
fmt.Println("Dog")
case Bird:
fmt.Println("Bird")
default:
fmt.Println("error")
}
}
func main() {
b := Bird{"鸟"}
d := Dog{"狗"}
fmt.Println(IsDog(b))
fmt.Println(IsDog(d))
WhatType(b)
WhatType(d)
}
执行结果
false
{狗}
true
Bird
Dog
3.4 空接口
空接口是一个比较特殊的类型,因为其内部没有定义任何方法所以空接口可以表示任何一个类型,比如可以进行下面的操作:
package main
import "fmt"
func main() {
var any interface{}
any = 1
fmt.Println(any)
any = "hello"
fmt.Println(any)
any = false
fmt.Println(any)
}
执行结果
1
hello
false