Go结构体与接口
一、结构体(struct)
相当于Java中的类,结构体名称首字母需大写,结构体名称首字母大写是公有的,包外可访问;首字母小写是私有的,仅在包内可访问
结构体命名需要使用驼峰命名法,且不能出现下划线
Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体(struct),Go语言中通过结构体来实现面向对象
go语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合
1)结构体定义
type struct_name struct{ var_name1 type1 var_name2 type2 ... }
如,定义一个User结构体:
type User struct { name string gender string address string age int }
同样类型的字段也可以写在一行
type User struct { name, gender string address string age int }
注意:结构体定义时结构体字段的可见性规则
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
2)结构体实例化
只有当结构体实例化时,才会真正地分配内存。
也就是必须实例化后才能使用结构体的字段
结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
结构体声明:
var 结构体实例 结构体类型
基本实例化,直接使用结构体变量:
// 声明结构体类型 var user User fmt.Println(reflect.TypeOf(user)) // main.User fmt.Println(user) // { 0} // 为结构体的成员变量赋值 user.name = "yangyongjie" fmt.Println(user) // {yangyongjie 0} // 访问结构体中的成员变量 fmt.Println(user.name) // yangyongjie
3)访问结构体成员
结构体名.成员名
4)匿名结构体
var user struct{ Name string; Age int} 或 var user struct{ name string gender string }
5)结构体指针
结构体指针类似于其他指针变量
var struct_pointer *struct_name
以上定义的结构体指针可以存储结构体变量的地址
struct_pointer = &struct_var_name // 取结构体变量的地址赋给结构体指针
6)创建指针类型结构体
使用new关键字对结构体进行实例化,得到的是结构体的内存地址
var user1 = new(User) fmt.Println(user1) // &{ 0} fmt.Println(reflect.TypeOf(user1)) // *main.User 返回的是指针类型 // Go语言中支持对结构体指针直接使用.来访问结构体的成员,user1.name其实在底层是(*user1).name = "hello",这是Go语言帮我们实现的语法糖 user1.name = "hello" fmt.Println(user1) // &{hello 0}
7)取结构体的地址实例化
使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作
user2 := &User{} fmt.Println(reflect.TypeOf(user2)) // *main.User //user2.address 其实在底层是(*user2).name = "nanjing",这是Go语言帮我们实现的语法糖 user2.address = "nanjing" fmt.Println(user2) // &{ nanjing 0}
8)结构体初始化
①:直接使用结构体变量
初始化的是结构体类型,直接打印返回的是结构体的值
var user1 User
user1.name = "test"
user1.address = "beijing"
fmt.Println(reflect.TypeOf(user1)) // main.User
fmt.Println(user1) // {test 0 beijing}
②:使用new关键字
初始化的是指针类型,直接打印返回的是内存地址
user := new(User)
user.name = "yangyongjie"
user.age = 18
fmt.Println(reflect.TypeOf(user)) // *main.User
fmt.Println(user) // &{yangyongjie 18 }
③:使用键值对初始化
对结构体使用键值对初始化时,键对应结构体的字段,值对应该字段的初始值
初始化的是结构体类型,直接打印返回的是结构体的值
user3 := User{ name: "yangyongjie", age: 27, gender: "male", address: "nanjing", } fmt.Println(reflect.TypeOf(user3)) // main.User fmt.Println(user3) // {yangyongjie male nanjing 27}
④:也可以对结构体指针进行键值对初始化,
初始化的是指针类型,直接打印返回的是内存地址。如:
user4 := &User{ name: "yangyongjie", age: 27, gender: "male", address: "nanjing", } fmt.Println(reflect.TypeOf(user4)) // *main.User fmt.Println(user4) // &{yangyongjie male nanjing 27}
工作中一般使用 u:= &User{name: "yangyongjie",...} 这种方式实例化并初始化
9)构造函数
Go语言的结构体没有构造函数,但是我们可以自己实现,如:
func newUser(name, gender, address string, age int) *User { return &User{ name: name, age: age, gender: gender, address: address, } }
调用构造函数:
user5 := newUser("yangyongjie", "male", "nanjing", 27) fmt.Println(user5) // &{yangyongjie male nanjing 27}
10)结构体作为函数参数
可以像其他数据类型一样将结构体类型作为参数传递给函数。并以以上实例的方式访问结构体变量
type Books struct { title string author string subject string book_id int } func printBook(book Books) { fmt.Printf( "Book title : %s\n", book.title) }
11)方法和接收者
Go 语言中同时有函数和方法。一个方法就是一个包含了接收者的函数。
方法可以将类型和方法封装在一起,实现强耦合。
接收者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集
Go语言中的方法时一种作用于特定类型变量的函数。这种特定类型变量叫做接收者,接收者的概念类似于Java语言中的this,和Python语言中的self。只不过Go语言中需要将this显式的声明出来。
接收者可以理解为当前的对象,即方法所在类型的对象,可以理解为结构体的方法,类似于Java中的this,显式的声明了出来
方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回类型) {
方法体
}
示例:
// 定义结构体
type User struct {
name string
gender string
address string
age int
}
// 该方法属于User对象中的方法
// 值类型的接收者
func (user User) getName() string {
// name属性即user对象中的属性
return user.name
}
// 指针类型的接收者
func (user *User) setName(name string) {
user.name = name
}
func main() {
user := &User{
name: "yangyongjie",
age: 27,
gender: "male",
address: "nanjing",
}
name := user.getName() // 该方法只能User结构体类型的变量或指针才能调用
fmt.Println(name) // yangyongjie
user1 := &User{}
user1.setName("yyj")
fmt.Println(user1.name) // yyj
}
值类型的接收者和值类型的接收者方法的区别:
指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式十分接近于Java语言中的this,和Python语言中的self
当方法作用于值类型的接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但是修改操作只是针对副本,无法修改接收者变量本身。
import "fmt" // 定义结构体 type User struct { name string gender string address string age int } // 值类型的接收者 func (user User) setAddress(address string) { user.address = address } // 指针类型的接收者 func (user *User) setName(name string) { user.name = name } func main() { user := &User{ name: "yangyongjie", age: 27, gender: "male", address: "nanjing", } // 接收值类型的方法,user变量本身值没有被修改 user.setAddress("beijing") fmt.Println(user.address) // nanjing // 接收指针类型的方法,user变量本身值没有被修改 user.setName("yyj") fmt.Println(user.name) // yyj }
什么时候应该使用指针类型接收者?
①:需要修改接收者中的值
②:接收者是拷贝代价比较大的大对象
③:保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者
结构体变量和结构体指针的理解:
结构体指针指向的是结构体变量的内存地址
结构体变量是结构体类型的变量本身的值
12)结构体的"继承"
一个结构体中可以嵌套包含另一个结构体或结构体指针
通过嵌套匿名结构体(结构体指针)实现继承
//Animal 动物 type Animal struct { name string } //Dog 狗 type Dog struct { Feet int8 *Animal //通过嵌套匿名结构体实现继承 }
13)结构体与JSON序列化
user := &User{ Name: "yangyongjie", Age: 27, Gender: "male", Address: "nanjing", } // 结构体JSON序列化 data, err := json.Marshal(user) if err != nil { fmt.Println("json marshal failed") return } fmt.Printf("%s\n", data) // {"Name":"yangyongjie","Gender":"male","Address":"nanjing","Age":27} // JSON反序列化结构体 user1 := &User{} err = json.Unmarshal(data, user1) if err != nil { fmt.Println("json unmarshal failed!") return } fmt.Print(user1) // &{yangyongjie male nanjing 27}
二、接口
接口(interface,相当于Java中的接口,接口名称首字母大写)
go语言提供的一种数据类型,接口将所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
在Go语言中接口(interface)是一种类型,一种抽象的类型。请牢记在Go语言中接口(interface)是一种类型
1)定义接口:
type interface_name interface{
method1() [return_type]
method2() [return_type]
method3() [return_type]
}
2)实现接口方法:
func (struct_name_variable struct_name) method1() [return_type] { // 方法实现 }
3)需要注意:
空接口可以作为任何类型数据的容器
一个类型可实现多个接口
接口命名习惯以 er 结尾
接口只有方法声明,没有实现,没有数据字段
当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问
接口也可实现类似OOP中的多态
只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗
4)接口嵌套
接口与接口间可以通过嵌套创造出新的接口
需要注意的是,嵌入其他接口类型不能有同名方法,也不能嵌入自身或循环嵌入
// Sayer 接口 type Sayer interface { say() } // Mover 接口 type Mover interface { move() } // 接口嵌套 type animal interface { Sayer Mover }
5)空接口(interface{})
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口,空接口可以存储任意类型的值。
因为空接口可以存储任意类型值的特点,所以空接口在Go语言中的使用十分广泛
空接口应用:
①:空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数
// 空接口作为函数参数 func show(a interface{}) { ... }
②:空接口作为map的值
map使用空接口可以保存任意类型的值
// 空接口作为map值 var studentInfo = make(map[string]interface{}) studentInfo["name"] = "李白" studentInfo["age"] = 18 studentInfo["married"] = false fmt.Println(studentInfo)
类型断言:
一个接口类型的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。
这两部分分别称为接口的动态类型和动态值
想要判断空接口中的值的动态类型是否满足指定的类型可以使用类型断言,语法格式为:
x.(T)
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败
func main() { var x interface{} x = "pprof.cn" v, ok := x.(string) if ok { fmt.Println(v) } else { fmt.Println("类型断言失败") } }
6)接口使用技巧
①:尽量定义包含方法少的接口,建议控制接口方法数量不超过 3 个
我们可以在一些 Golang 语言标准库中发现,很多接口包含的方法数量都不超过 3 个,也有很多接口仅包含 1 个方法
控制接口包含方法的数量尽量少的好处是接口包含的方法越少,越容易实现和组合
②:尽量不使用空接口类型作为函数参数
Golang 语言是强类型静态语言,Golang 编译器在编译期间会对变量做类型检查。如果函数或方法接收的参数类型是空接口 interface{},编译器将收不到任何信息,也就不会对空接口类型的变量进行类型检查,接收参数的类型将需要开发者自己做类型检查。所以开发者尽量不要使用空接口 interface{} 变量作为接收参数
但是空接口 interface{} 类型也并非完全无用武之地,因为目前 Golang 语言(v1.16.4)还未支持泛型,当需要处理未知类型的参数时,可以使用空接口 interface{} 类型,在 Golang 语言标准库中也有该使用方式,比如 fmt 包
END.