土拨鼠--> 面向对象(struct and interface)
Go语言面向对象概述
(1)学过c++/java/C# 的人应该都知道,面向对象的三个基本特征:封装、继承和多态,go也支持面向对象编程,但并不是纯粹的面向对象语言,只能说支持面向对象编程的特性;
(2)go没有类(class)概念,go语言的结构体(struct)等同于其他编程语言的类(class),可以说是基于结构体(struct)来实现OOP特性;
(3)go语言通过结构体内嵌、接口(interface)来实现面向对象的三大特性(封装、继承和多态);
(4)go语言面向对象编程非常简洁,去掉了传统OOP语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等特性。
结构体定义
go语言提供了自定义数据类型,可以封装多个基本数据类型,这种数据类型叫做struct(结构体)。
type 类型名 struct { 字段名 字段类型 字段名 字段类型 … }
实例化
只有当结构体实例化时,才会真正地分配内存,也就是说结构体必须实例化后才可使用。
//结构体定义 type person struct { name string city string age int } //结构体实例化 func main() { var p1 person p1.name = "juju" p1.city = "深圳" p1.age = 18 fmt.Println(p1) }
匿名结构体
定义匿名结构体时没有type关键字,与其他定义类型地变量一样,如果在函数外部需在结构体变量前加上var关键字,在函数内部可省略 var关键字。
package main import ( "fmt" ) func main() { var user struct{ Name string Age int } user.Name = "juju" user.Age = 18 fmt.Printf("%#v\n", user) }
指针类型结构体
可以通过new关键字对结构体进行实例化,得到结构体的地址。
type person struct { name string city string } func main(){ var p2 = new(person) fmt.Printf("%T\n", p2) //*main.person fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:""} }
使用&对结构体进行取地址操作相当于对结构体类型进行了一次new实例化操作。
p3 := &person{} fmt.Printf("%T\n", p3) //*main.person fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:""}
没有初始化的结构体,其成员变量都是对应其类型的零值,当某些字段没有初始值的时候,该字段可以不写,此时没有初始值的字段默认为其类型的零值。
//使用键值对初始化 p4 := person{ name: "juju", city: "深圳", age: 18, } //结构体指针进行键值对初始化 p5 := &person{ name: "juju", city: "深圳", age: 18, } //某些字段没有初始值 p6 := &person{ city: "深圳", } fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"", city:"深圳", age:0}
结构体内存
结构体占用的是一块连续的内存。
type test struct { i int8 j int8 k int8 } n := test{ 1, 2, 3, } fmt.Printf("n.i %p\n", &n.i) fmt.Printf("n.j %p\n", &n.j) fmt.Printf("n.k %p\n", &n.k) //输出 n.i 0xc0000100d8 n.j 0xc0000100d9 n.k 0xc0000100da
方法和接收者
go语言中的方法(Method)是一种作用于特定类型变量的函数,不仅仅是结构体,任何类型都可以拥有方法,这种特定类型变量
叫做接收者(Receiver),方法的定义格式如下:
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
接收者命名使用接收者类型名称首字母的小写
type Person struct { name string age int8 } //Hobby是Person类型的方法 func (p Person) Hobby() { fmt.Printf("%s的爱好是跑步!\n", p.name) } //SetAge 修改年龄的方法 func (p *Person) SetAge(age int8) { p.age = age } func main() { p1 := &Person{
name:"juju",
age:18,
}
p1.Hobby()
fmt.Println(p1.age) // 18
p1.SetAge2(20)
fmt.Println(p1.age) // 20
}
当方法作用于值类型接收者时,程序运行时将接收者的值复制一份,在接收者的方法中可以获取接收者的成员值,单修改操作只是副本,无法真正修改接收者变量本身;而指针接收者,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。使用指针接收者的情况如下:
a.需要修改接收者中的值;
b.接收者是拷贝代价比较大的对象;
c.保证一致性,如果某个方法使用了指针接收者,那么其他方法也应该使用指针接收者。
嵌套结构体
一个结构体可以嵌套另外一个结构体或结构体指针。
//Address 地址结构体 type Address struct { City string } //User 用户结构体 type User struct { Name string Address Address } func main() { user1 := User{ Name: "juju", Address: Address{ City: "深圳", }, } fmt.Println(user1) }
嵌套匿名结构体
//Address 地址结构体 type Address struct { Province string City string } //User 用户结构体 type User struct { Name string Gender string Address //匿名结构体 } func main() { var user2 User user2.Name = "juju" user2.Gender = "男" user2.Address.Province = "广东" //通过匿名结构体.字段名访问 user2.City = "深圳" //直接访问匿名结构体的字段名 fmt.Printf("user2=%#v\n", user2) }
注意:
a.访问结构体成员时会优先在结构体中查找该字段,找不到再去匿名结构体中查找;
b.当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如果希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分;
c.如果嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分。
构造函数
go语言没有构造函数,是通过工厂模式来解决这个问题。工厂模式就是为了解决变量的首字母为小写的结构体能够被其它包引用的问题。当结构体比较复杂的话,值拷贝性能开销比较大,这时该构造函数返回结构体指针比较合适。
func NewPerson(name, city string, age int8) *person {
return &person{//相当于new得到的是一个指针
name: name,
city: city,
age: age,
}
}
func main(){
p:=NewPersion("juju","深圳",18)
fmt.Println(p)
}
注意:结构体指针做函数返回值,不能返回局部变量的地址值,局部变量保存在栈帧上,函数调用结束后,栈帧释放,局部变量的地址不再受系统保护,随时可能分配给其他程序使用,可以返回局部变量的值。
封装
封装实现:
a.将结构体、字段(属性)的首字母小写(其他包不能使用);
b.给结构体所在包提供一个工厂模式的构造函数,首字母大写;
c.提供一个首字母大写的Set方法,用于对属性的判断和赋值;
d.提供一个首字母大写的Get方法,用于获取属性值
//定义一个person的结构体 type person struct { Name string age int } //写一个工厂模式函数,类似构造函数 func NewPerson(name string,age int) *person { return &person{ Name : name, age : age } } //为了访问age 和 sala,以age字段为例编写对应的SetXxx和GetXxx方法 func (p *person) SetAge(age int) { if age > 0 && age < 150 { p.age = age } else { fmt.Println("年龄范围不正确...") } } func (p *person) GetAge() int { return p.age }
继承
通过使用嵌套匿名结构体实现。
//Animal 动物 type Animal struct { name string } func (a *Animal) move() { fmt.Printf("%s会动!\n", a.name) } //Person 结构体 type Person struct { name string age int8 *Animal //通过嵌套匿名结构体实现继承,也可以使用时指针 } func (d *Person ) Think() { fmt.Printf("%s低头思考。。。。\n", d.name) } func main() { p := &Person { name: "juju", age:18, Animal: &Animal{ //注意嵌套的是结构体指针 name: "juju", }, } p.wang() p.move() }
结构体与Json
结构体和Json有着类似的键值对,所以其可以互相转换,注意的是结构体里面的字段首字母一定要大写,否则转换成json为空。
//Person 结构体 type Person struct { Name string Age int8 Hobby []string } func main() { p:= &Person { Name : "juju", Age : 18, Hobby :{"跑步","游泳","拍照"}, } //JSON序列化:结构体-->JSON格式的字符串 data, err := json.Marshal(p) if err != nil { fmt.Println("json marshal failed") return } fmt.Printf("json:%s\n", data) //JSON反序列化:JSON格式的字符串-->结构体 p1 := &Person {} err = json.Unmarshal([]byte(data), p1) if err != nil { fmt.Println("json unmarshal failed!") return } fmt.Printf("%#v\n", p1) }
结构体标签
Tag是结构体的元信息,可以在运行时通过反射的机制读取出来,在结构体字段的后方定义,由一对反引号包起来;使用Tag必须严格遵守键值对的规则,因为如果格式错误,编译和运行时都不会提示任何错误,最后导致反射无法得到正确值,如不要再key和value间有空格。
//Student 学生 type Student struct { ID int `json:"id"` //通过指定tag实现json序列化该字段时的key Gender string //json序列化是默认使用字段名作为key name string //私有不能被json包访问 } func main() { s1 := Student{ ID: 1, Gender: "男", name: "juju", } data, err := json.Marshal(s1) if err != nil { fmt.Println("json marshal failed!") return } fmt.Printf("json str:%s\n", data) }
接口(interface)
go语言中接口是一种类型。它定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。接口区别于我们之前所有的具体类型,接口是一种抽象的类型,接口不关心事物的属性,只关心事物具有的行为,go语言提倡面向接口编程。其定义如下:
type 接口类型名 interface{ 方法名1( 参数列表1 ) 返回值列表1 方法名2( 参数列表2 ) 返回值列表2 … }
//注意:接口内部不能有变量,只有方法或者不写任何方法
接口的实现
一个对象只要实现了接口中所有的方法,那么就实现了这个接口。
//定义一个Sayer的接口 type Sayer interface { Say() } //定义两个结构体Cat和Dog type Cat struct {} type Dog struct {} type Bird struct{} // Dog实现了Sayer接口 func (d Dog) Say() { fmt.Println("汪汪") } // Cat实现了Sayer接口 func (c Cat) Say() { fmt.Println("喵喵") } // Bird 实现了Sayer接口 func (b Bird ) Say() { fmt.Println("叽叽叽") } func main() { var x Sayer //声明一个Sayer类型的变量x a := Cat{} b := Dog{} c :=&Bird {} x = a //只有自定义类型实现了某个接口,才能将自定义类型的变量赋值给接口类型变量 x.Say() x = b x.Say() x=c //x可以接收*Bird类型 x.Say() }
由以上例子可以得出,如果接收者是值类型,不管是结构体还是结构体指针类型的变量,只要这个结构体实现对应的接口,那么这两个变量都可以赋值给该接口变量。因为Go语言中有对指针类型变量求值的语法糖,*Bird等价于Bird。
//声明一个Mover的接口 type Mover interface { move() } func (d *dog) move() { fmt.Println("狗会动") } func main() { var x Mover var dog1 = dog{} x = dog1 //x不可以接收dog类型,会报dog类型没有实现Mover接口 var dog2 = &dog{} x = dog2 //x可以接收*dog类型 x.move }
由以上例子可知,如果接收者是指针类型,只能将结构体指针赋值给接口类型的变量。
空接口
空接口是没有定义任何方法的接口,所以任何类型都可以实现空接口。空接口类型变量可以存储任意类型的变量。
func main() { // 定义一个空接口x var x interface{} s := "Hello juju" x = s fmt.Printf("type:%T value:%v\n", x, x) i := 100 x = i fmt.Printf("type:%T value:%v\n", x, x) b := true x = b fmt.Printf("type:%T value:%v\n", x, x) }
使用空接口可以接收任意类型的函数参数(典型样例:fmt.Println()函数):
// 空接口作为函数参数 func printMsg(x interface{}) { fmt.Printf("type:%T value:%v\n", x, x) }
使用空接口可以保存任意值的字典:
// 空接口作为map值 var InfoMsg = make(map[string]interface{}) InfoMsg ["name"] = "juju" InfoMsg ["age"] = 18 InfoMsg ["married"] = false fmt.Println(InfoMsg )
接口和继承区别
a.接口和继承解决的问题不同:继承在于解决代码的复用性和可维护性,接口在于设计,设计好各种规范(方法),让其他自定义类型去实现这些方法;
b.接口比继承更加灵活;
c.接口在一定程度上实现代码解耦。
多态
在go语言中多态是通过接口实现的,可以按照统一的接口来调用不同的实现,这时接口变量就呈现不同的形态。
//声明/定义一个接口 type Usb interface { //声明了两个没有实现的方法 Start() Stop() } type Phone struct { } //让Phone 实现 Usb接口的方法 func (p Phone) Start() { fmt.Println("手机开始工作。。。") } func (p Phone) Stop() { fmt.Println("手机停止工作。。。") } type Camera struct { } //让Camera 实现 Usb接口的方法 func (c Camera) Start() { fmt.Println("相机开始工作~~~。。。") } func (c Camera) Stop() { fmt.Println("相机停止工作。。。") } //计算机 type Computer struct { } //编写一个方法Working 方法,接收一个Usb接口类型变量 //只要是实现了 Usb接口 (所谓实现Usb接口,就是指实现了 Usb接口声明所有方法) func (c Computer) Working(usb Usb) {//通过usb接口变量来调用Start和Stop方法 usb.Start() usb.Stop() } func main() { //测试 //先创建结构体变量 computer := Computer{} phone := Phone{} camera := Camera{} //关键点,既可以接收手机变量,又可以接收相机变量 computer.Working(phone) computer.Working(camera) }
类型断言
类型断言基本语法:x.(T)
类型断言(带检测):
if y, ok := x.(T); ok {
.........
}
循环判断传入参数的类型:
//编写一个函数,可以判断输入的参数是什么类型 func TypeJudge(items... interface{}) { for i, x := range items { switch x.(type) { case bool : fmt.Printf("第%v个参数是 bool 类型,值是%v\n", i, x) case float32 : fmt.Printf("第%v个参数是 float32 类型,值是%v\n", i, x) case float64 : fmt.Printf("第%v个参数是 float64 类型,值是%v\n", i, x) case int, int32, int64 : fmt.Printf("第%v个参数是 整数 类型,值是%v\n", i, x) case string : fmt.Printf("第%v个参数是 string 类型,值是%v\n", i, x) default : fmt.Printf("第%v个参数是 类型 不确定,值是%v\n", i, x) } } } func main() { var n1 float32 = 1.5 var n2 float64 = 3.2 var n3 int32 = 50 var name string = "juju" address := "深圳" n4 := 500 TypeJudge(n1, n2, n3, name, address, n4, stu1, stu2) }