一、面向对象
1.1、初识golang面向对象
golang支持面向对象编程(OOP)。
golang没有类(class),go语言的结构体(struct)和其他编程语言的类(class)有同等的地位。简单理解golang是根据struct来实现面向对象编程(OOP)的。
golang有面向对象的继承,封装和多态的特性
golang面向对象很优雅,通过(interface)关联,耦合性低,很灵活。也就是说golang中面向接口编程是非常重要的特性。
二、面向对象-结构体(struct)和结构体变量(值类型)
2.1、结构体(struct)和结构体变量的快速入门(更多方式:详见2.4)
快速理解:可以把"结构体"理解为"类","结构体变量"理解为"对象"
// 定义结构体变量,也叫做"对象" type Cat struct { Name string Age int Color string } func main() { // 创建一个结构体变量"cat",类型为"Cat",是一个struct var cat1 Cat fmt.Println(cat1) // 返回的默认值:{ 0 } ,string默认值为"",int默认值为0,所以这里显示只有一个0 // 给变量的"属性"赋值 cat1.Color="yellow" cat1.Age=2 cat1.Name="dandan" fmt.Println(cat1) // 返回值:{dandan 2 yellow} 分别对应Name,Age,Color的值 fmt.Println(cat1.Name) // 返回值:dandan fmt.Println(cat1.Age) // 返回值:2 fmt.Println(cat1.Color) // 返回值:yellow fmt.Printf("%T",cat1) // 类型:main.Cat }
2.2、结构体变量在内存中的布局(重要)
1.声明一个结构体变量时,数据结构已经存在了,数据的值就是不同类型的默认值(string默认为空,int类型默认为0);
2.结构体变量是值类型(相互数据不影响,如果想要相互数据影响的话,详见:2.5)
2.3、结构体(struct)声明
type 结构体名称 struct {
field1 type
field2 type
}
2.3.1、结构体(struct)声明例子
结构体名称首字母大写,那么结构体可以在其他包被使用
结构体(字段|属性)首字母大写,那么结构体字段可以在其他包被使用
不同结构体变量的字段是独立的,互不影响
type Cat struct {
Name string // (字段|属性)是string类型
Age int // (字段|属性)是int类型
Color string // (字段|属性)是string类型
}2.3.2、创建一个结构体变量后,如果没有给字段赋值,那么会有一个默认值:
布尔类型的默认值是:false
string类型的默认值是:""
int类型的默认值是:0
指针,slice(切片)和map的默认值都是nil,即没有分配内存空间(在使用时,需要先make)
例子如下:
// 定义结构体变量,也叫做"对象" type Test struct { par1 *int // 指针 slice1 []int // 切片 map1 map[string]string // map } func main() { // 定义结构体变量 var test Test fmt.Println(test.par1) // 默认值:<nil> fmt.Println(test.slice1) // 默认值:[] fmt.Println(test.map1) // 默认值:map[] if test.par1 == nil { fmt.Println("test.par1 is nil") } if test.slice1 == nil { fmt.Println("test.slice1 is nil") } if test.map1 == nil { fmt.Println("test.map1 is nil") } // 依次输出: // test.par1 is nil // test.slice1 is nil // test.map1 is nil // 此时给"结构体变量"直接赋值,一定会报错。需要先给切片make,在使用 test.slice1=make([]int,2) test.slice1[0]=111 test.slice1[1]=222 fmt.Println(test.slice1) // [111 222] //需要先给map make,在使用 test.map1=make(map[string]string) test.map1["name"]="sudada" test.map1["age"]="18" fmt.Println(test.map1) // map[age:18 name:sudada] }
2.4、结构体变量的声明和结构体变量字段使用
2.4.1、方式1(先声明结构体变量,然后赋值)
var test struct
// 定义结构体变量,也叫做"对象" type Test struct { Name string Age int } func main() { var test Test test.Name="sudada" test.Age=18 fmt.Println(test) // {sudada 18} }
2.4.2、方式1的延伸(先声明结构体变量,然后赋值)
var test = struct{}
// 定义结构体变量,也叫做"对象" type Test struct { Name string Age int } func main() { var test = Test{} // 简写 test := Test{} test.Name="sudada" test.Age=18 fmt.Println(test) // {sudada 18} }
2.4.3、方式2(声明结构体变量,同时赋值)
var test = struct{Name: "sudada",Age: 18,}
// 定义结构体变量,也叫做"对象" type Test struct { Name string Age int } func main() { var test = Test{ Name: "sudada", Age: 18, } // 或者简写 test := Test{ Name: "sudada", Age: 18, } // 或者这样写 var test = Test{"sudada",18} test := Test{"sudada",18} fmt.Println(test) // {sudada 18} }
2.4.4、方式3(结构体变量是一个指针时,然后赋值)
var test *struct = new(struct)
// 定义结构体变量,也叫做"对象" type Test struct { Name string Age int } func main() { // 结构体变量是一个"指针" var test *Test = new(Test) // 可以简写成:var test = new(Test) // 给"指针"类型的"结构体变量"字段赋值的方式(标准写法) (*test).Name="sudada" (*test).Age=18 fmt.Println(*test) // {sudada 18} // go为了程序使用方便,针对"指针"类型的结构体变量,底层做了优化:test.Age=18 == (*test).Age=18 test.Name="wang" test.Age=28 fmt.Println(*test) // {wang 28} }
2.4.5、方式3的延伸(结构体变量是一个指针时,然后赋值)
var test *struct = &struct{}
// 定义结构体变量,也叫做"对象" type Test struct { Name string Age int } func main() { // 结构体变量是一个"指针" var test *Test = &Test{} // 可以简写成:var test = &Test{} // 给"指针"类型的"结构体变量"字段赋值的方式(标准写法) (*test).Name="sudada" (*test).Age=18 fmt.Println(*test) // {sudada 18} // go为了程序使用方便,针对"指针"类型的结构体变量,底层做了优化:test.Age=18 == (*test).Age=18 test.Name="wang" test.Age=28 fmt.Println(*test) // {wang 28} }
2.4.6、方式4(结构体变量是一个指针,同时赋值)
var test *struct = &Test{Name:"sudada", Age:28,}
// 定义结构体变量,也叫做"对象" type Test struct { Name string Age int } func main() { // 结构体变量是一个"指针",声明时赋值 var test *Test = &Test{ Name:"sudada", Age:28, } fmt.Println(*test) // {sudada 28} // go为了程序使用方便,针对"指针"类型的结构体变量,底层做了优化:test.Age=18 == (*test).Age=18 test.Name="wang" test.Age=28 fmt.Println(*test) // {wang 28} }
2.5、结构体变量将指针赋值给另外一个结构体变量时,内存分配机制
"test2指针"对应的"值"是"test1的指针地址"(那么修改test1或者test2时,2边的值都会跟着改变)
// 定义结构体变量,也叫做"对象" type Test struct { Name string Age int } func main() { var test1 = Test{} test1.Name="sudada" test1.Age=18 // 把test1的"指针"赋值给test2 fmt.Printf("%p\n",&test1) // test1的指针是:0xc000008048 var test2 *Test = &test1 fmt.Printf("%p\n",&test2) // test2的指针是:0xc00006a028(test2的指针存放的值就是test1的指针) fmt.Printf("%p\n",test2) // test2的值(等于test1的指针)是:0xc000008048 fmt.Println(*test2) // test2:{sudada 18} // 修改"指针"的值时,test1和test2对应的值都会改变 test2.Name="wang" fmt.Println(test1) // test1:{wang 18} fmt.Println(*test2) // test2:{wang 18} }
2.6、结构体struct 注意事项
2.6.1、结构体和其他类型进行转换时,需要有完全相同的字段(字段名字,字段个数和字段类型)
type A struct { Name string Age int } type B struct { Name string Age int } func main() { var a A var b B // 这样强制转换不会报错(A B结构体字段名称,字段个数和字段类型相同) a = A(b) fmt.Println(a,b) // { 0} { 0} }
2.6.2、结构体进行type重新定义(相当于取别名),goloang认为是新的数据,但是相互间可以强转
// 结构体1 type Student struct { Name string Age int } // 结构体2 type Stu Student func main() { var stu1 Student var stu2 Stu // 这样强制转换不会报错(Student Stu结构体字段名称,字段个数和字段类型相同) stu1 = Student(stu2) fmt.Println(stu1,stu2) // { 0} { 0} }
2.6.3、结构体的每个字段都可以写一个tag,该tag可以通过反射机制获取
type student struct {
Name string `json:"name"`
Age int `json:"age"`
Sex string `json:"sex"`
}import ( "encoding/json" "fmt" ) // 结构体1 type student struct { Name string `json:"name"` Age int `json:"age"` Sex string `json:"sex"` } func main() { var stu1 = student{"sudada",18,"nan",} fmt.Println(stu1) // {sudada 18 nan} // 把结构体变量stu1序列化(这里传入"结构体字段"必须大写,如果不大写"结构体字段",那么json.Marshal读取不到值) jsonStr, err := json.Marshal(stu1) if err != nil { fmt.Println("报错: ",err) } else { fmt.Println(string(jsonStr)) // 默认返回的是bytes类型的切片,需要使用string做一下转换 // 返回值(json格式):{"Name":"sudada","Age":18,"Sex":"nan"} // 结构体字段使用tag之后: // 返回值(json格式):{"name":"sudada","age":18,"sex":"nan"} } }
三、面向对象-方法
3.1、什么是方法?
glang中的方法是作用在指定的数据类型上的,即:方法和指定的数据类型绑定,因此自定义类型,都可以有方法。而不仅仅是struct。
3.2、方法的声明和调用
快速理解方法:可以把"结构体"理解为"类","结构体变量"理解为"对象",方法就是"类"里面定义的函数。
方法被对象调用时:对象.方法(),会把对象本身作为实参传递给方法(默认是值传递,修改后不改变原来的值)
方法的声明:
func (name 结构体名称) 方法名 (实参 实参类型) (返回值 返回值类型){
代码段
return 返回值
}
方法的案例1:
// 定义"结构体类型Person" type Person struct { Name string age int } // "结构体类型Person"的方法,名为"test" // (person Person)表示:test方法,是给"结构体类型Person"绑定的 func (person Person) test() { // 这里的person的值就等于sudada的值(值拷贝),但是修改person的值,不会影响sudada的值,例子如下: person.Name = "Jack" fmt.Println("test",person.Name) // 返回值:"Jack" } func main() { // 给"结构体变量aaa"赋值 var sudada = Person { Name: "sudada", age: 18, } // 调用"结构体类型Person"的方法"test" sudada.test() // sudada调用test时会把sudada传入test方法内(谁调用就把谁传入),值拷贝 fmt.Println("main",sudada.Name) // 返回值:"sudada" // A.test() // 不能直接调用"结构体类型Person"的方法"test" // test() // 直接调用,会把"test"作为一个函数,而不是方法 }
3.3、方法的快速入门
3.3.1、案例1:给Person结构体添加speak方法(无参数无返回值)
// 定义"结构体类型Person" type Person struct { Name string age int } // "结构体类型Person"的方法,名为"speak" func (person Person) speak() { fmt.Println(person.Name, "is good man") // 返回值:sudada is good man } func main() { var p = Person{ Name: "sudada", age: 18, } p.speak() }
3.3.2、案例2:给Person结构体添加"jisuan"方法(把方法当做函数一样,接收参数并返回值)
// 定义"结构体类型Person" type Person struct { Name string age int } // "结构体类型Person"的方法,名为"jisuan" func (person Person) jisuan(num int) (sum int) { res:=0 for i:=0;i<=num;i++{ res+=i } return res } func main() { var p = Person{ Name: "sudada", age: 18, } var res int res = p.jisuan(1000) fmt.Println("求和为",res) // 求和为 500500 }
3.4、方法的调用和传参机制
方法的调用和传参机制,和函数基本一样;
不一样的地方是:对象.方法(),会把对象本身作为实参传递给方法(默认是值传递,修改后不改变原来的值)
3.5、方法的注意事项(重要★★★★★)
3.5.1、结构体类型是值类型,在方法调用中,使用的是值拷贝的传递方式。
3.5.2、为了提高效率,通常将方法和结构体的指针类型绑定,例子:
// 定义"结构体类型Person" type Person struct { Name string age int } // "结构体类型Person"的方法,名为"test" // 这里的person作为一个指针类型 func (person *Person) test() { // 标准写法:(*person).Name = "Jack" // 因为编译器做了优化,可以简写如下: person.Name = "Jack" fmt.Println(person.Name) // 返回值:Jack } func main() { var p = Person{ Name: "sudada", age: 18, } // 标准写法:(&p).test() // 因为编译器做了优化,可以简写如下: p.test() fmt.Println(p.Name) // 返回值:Jack }
3.5.3、glang中的方法是作用在指定的数据类型上的,即:方法和指定的数据类型绑定,因此自定义类型,都可以有方法。而不仅仅是struct。比如int,float等也可以有方法。
给int类型创建方法,例子:
type Integer int func (i Integer)test() { fmt.Println(i) // 返回值:10 } // 绑定指针,修改值后,其他地方也会跟着修改 func (i *Integer)change() { *i += 1 } func main() { var i Integer = 10 i.test() i.change() fmt.Println(i) // 返回值:11 }
3.5.4、方法名大写,可以在其他包使用,反之小写,只能在当前包使用。
3.5.5、如果一个类型实现了String()这个方法,那么调用fmt.Println时,默认会调用这个变量的String()方法进行输出。(输出日志时可以使用),案例:
type Student struct { Name string Age int } func (stu Student) String() string { msg := fmt.Sprintf("Name=[%v] Age=[%v]", stu.Name, stu.Age) return msg } func main() { var stu1 Student = Student{ Name: "sudada", Age: 18, } // 调用fmt.Println方法 fmt.Println(stu1) // 返回值:Name=[sudada] Age=[18] }
3.6、方法和函数的区别(重要★★★★★)
3.6.1、调用方式不一样
函数调用:函数名(参数)
方法调用:方法绑定的类型变量.方法名(参数)
3.6.2、普通函数:函数接收的参数类型为值类型时,不能将指针类型的数据直接传递。反之:函数接收的参数类型为指针类型时,不能将值类型的数据直接传递。例子如下:
package main import "fmt" type Student struct { Name string } // 函数接收的参数类型为值类型时,不能将指针类型的数据直接传递 func test1(szq Student) { fmt.Println(szq.Name) } // 反之:函数接收的参数类型为指针类型时,不能将值类型的数据直接传递 func test2(szq *Student) { fmt.Println(szq.Name) } func main() { var stu = Student{ Name: "sudada", } test1(stu) // 这里"实参的类"型,必须根据函数"形参接收的类型"去传递 test2(&stu) // 这里"实参的类"型,必须根据函数"形参接收的类型"去传递 }
3.6.3、方法(如struct方法):方法的接收值为值类型时,可以用指针类型的变量调用方法,反之:方法的接收值为指针类型时,可以用值类型的变量调用方法。例子如下:
func (stu *Student) test1() {} 指针传递
func (stu Student) test1() {} 值传递package main import "fmt" type Student struct { Name string } // 方法的接收值为指针类型时,可以用值类型的变量调用test1方法 func (stu *Student) test1() { fmt.Println(stu.Name) } // 方法的接收值为值类型时,可以用指针类型的变量调用test2方法 func (stu Student) test2() { stu.Name = "test2" fmt.Println(stu.Name) } func main() { var stu1 = Student{ Name: "sudada", } var stu2 = Student{ Name: "wang", } stu1.test1() // 方法接收的是"指针类型",但是"值类型的变量"也可以调用,返回值:sudada fmt.Println(stu1.Name) // 返回值:sudada (&stu2).test2() // 方法接收的是"值类型",但是"指针类型的变量"也可以调用(虽然使用了&stu2,但是本质上还是属于值传递,因为方法test2接收的类型是值类型),返回值:test2 // (&stu2).test2() 等价于 stu2.test2() fmt.Println(stu2.Name) // 返回值:wang(上述&stu2本质上还是属于值传递,因为方法test2接收的类型是值类型,所以变量stu2的值,并没有改变) }
四、面向对象-工厂模式
4.1、为什么会用到工厂模式?
golang的结构体没有构造函数,通常使用工厂模式来解决这个问题
4.1、怎么用工厂模式?
当结构体struct的首字母小写,同时又需要在别的包内使用这个结构体时,需要用到工厂模式。
案例一(封装的一种模式)
// model.student.go文件 package model type student struct { Name string age int } // "student结构体"首字母小写,只能当前文件内使用。 // 通过"工厂模式"可以将"student结构体"在其他包使用。 func NewStudent(name string, age int) *student { return &student{ Name: name, age: age, } } // 如果"age"字段首字母小写,那么其他包不可以直接访问"age"字段,可以提供一个方法获取"age"字段的值 func (n *student) GetAge() int { return n.age } // main.go文件 package main import ( "Study/go008/demo01/model" "fmt" ) func main() { // stu 是一个指针,同时也是"student结构体"的变量(指针) stu := model.NewStudent("sudada",18) fmt.Println(*stu) // 返回值:{sudada 18} fmt.Println(stu.Name) // 返回值:sudada // fmt.Println(stu.age) // "student结构体"的age字段是小写,这里不能使用 // 所以这里给"student结构体"绑定一个"GetAge方法(首字母大写) fmt.Println(stu.GetAge()) // "返回值:18 }
五、面向对象-三大特性
面向对象思想-抽象
5.1、封装
5.1.1、什么是封装?
封装就是把抽象出的字段,和对字段的操作封装在一起,将数据保护在内部,程序的其他包只有通过被授权的操作(方法)才能对字段进行操作。
5.1.2、怎么用封装?
1.对结构体中的属性进行封装;
2.通过方法,包,实现封装;
5.1.3、封装的实现步骤:
1.将结构体,字段,属性的首字母小写;
2.给结构体所在包提供一个工厂模式的函数,首字母大写;
3.提供一个首字母大写的Set方法,用于对属性判断并赋值;
4.提供一个首字母大写的Get方法,用于获取属性的值;
5.1.4、封装的快速入门,案例一
// model/person.go 文件 package model import "fmt" type person struct { Name string age int sal float64 } // 工厂模式,返回一个"struct结构体"对象 func NewPerson(name string) *person { return &person{ Name: name, } } // 设置年龄的"方法" func (p *person) SetAge(age int) { if age > 0 && age < 100 { p.age = age } else { fmt.Println("年龄范围不合法") // 给个默认值 p.age = 18 } } // 获取年龄的"方法" func (p *person) GetAge() int { return p.age } // 设置薪资的"方法" func (p *person) SetSal(sal float64) { if sal > 3000 && sal < 30000 { p.sal += sal } else { fmt.Println("薪资范围不对3000-30000") } } // 获取薪资的"方法" func (p *person) GetSal() float64 { return p.sal } // main.go 文件 package main import ( "Study/go009/model" "fmt" ) func main() { // person是一个"struct结构体"对象 person := model.NewPerson("sudada") // 调用"SetAge"方法,设置年龄 person.SetAge(22) // 调用"SetSal"方法,设置薪资 person.SetSal(28000) // 调用"GetAge"方法,获取年龄 person_age := person.GetAge() // 调用"GetSal"方法,获取薪资 person_sal := person.GetSal() // Name是首字母大写的字段,这里可以直接调用 fmt.Println(person.Name) // 返回值:sudada fmt.Println("Age", person_age) // 返回值:Age 22 fmt.Println("Sal", person_sal) // 返回值:Age 28000 }
5.2、继承
5.2.1、什么是继承?
继承可以解决代码复用,当多个结构体存在相同的属性(字段)和方法时,可以从这些结构体中抽象出结构体,在该结构体中定义这些相同的属性和方法。
5.2.2、怎么用继承?
语法:
// "学生"结构体 type Student struct { Name string Age int } // "中学"结构体 type School struct { Student // 继承"结构体Student" } // "大学"结构体 type University struct { Student // 继承"结构体Student" }
继承案例快速理解:
package main import "fmt" // "学生"结构体 type Student struct { Name string Age int } // "学生"结构体的方法"" func (stu *Student) score(num int) { fmt.Printf("学生:%v 的分数是:%v\n", stu.Name, num) } // "中学"结构体 type School struct { Student // 继承"结构体Student" } // "中学"结构体的方法"testing" func (s *School) testing() { fmt.Println("school") } // "大学"结构体 type University struct { Student // 继承"结构体Student" } // "大学"结构体的方法"testing" func (s *University) testing() { fmt.Println("university") } func main() { // 定义一个结构体变量"middleStu" middleStu := &School{} // 给结构体变量"middleStu"赋值(方式一) //middleStu.Student.Name = "sudada" //middleStu.Student.Age = 10 // 给结构体变量"middleStu"赋值(方式二,简写) middleStu.Name = "sudada" middleStu.Age = 10 middleStu.testing() // 返回值:school fmt.Println(*middleStu) // 返回值:{{sudada 10}} // 调用"继承的结构体Student的方法score" middleStu.score(80) // 返回值:学生:sudada 的分数是:80 // 定义一个结构体变量"UniStu" UniStu := &University{} // 给结构体变量"UniStu"赋值(方式一) //UniStu.Student.Name = "sudada" //UniStu.Student.Age = 20 // 给结构体变量"UniStu"赋值(方式二,简写) UniStu.Name = "sudada" UniStu.Age = 20 UniStu.testing() // 返回值:university fmt.Println(*UniStu) // 返回值:{{sudada 20}} // 调用"继承的结构体Student的方法score" middleStu.score(150) // 返回值:学生:sudada 的分数是:150 }
5.2.3、继承的深入理解
1.结构体可以使用"父"结构体所有的字段和方法,即:首字母大写或者小写的字段,方法,都可以使用;
package main import "fmt" // "父"结构体 type Student struct { Name string age int } // "父"结构体的方法"score" func (stu *Student) score(num int) { fmt.Printf("学生:%v 的分数是:%v\n", stu.Name, num) } // 结构体School 继承"父"结构体Student type School struct { Student // 继承"结构体Student" } func main() { // 定义一个结构体变量"middleStu" middleStu := School{} // 结构体School使用"父"结构体Student的 字段 middleStu.Name = "sudada" middleStu.age = 18 fmt.Println(middleStu.Name) // 返回值:sudada fmt.Println(middleStu.age) // 返回值:18 // 结构体School使用"父"结构体Student的 方法 middleStu.score(80) // 返回值:学生:sudada 的分数是:80 }
2."父"结构体字段可以简化;
package main import "fmt" // "父"结构体 type Student struct { Name string age int } // 结构体School 继承"父"结构体Student type School struct { Student // 继承"结构体Student" } func main() { // 定义一个结构体变量"middleStu" middleStu := School{} // 变量赋值 middleStu.Student.Name = "sudada" middleStu.Student.age = 18 fmt.Println(middleStu.Student.Name) // 返回值:sudada fmt.Println(middleStu.Student.age) // 返回值:18 // 变量赋值,可以简写为: middleStu.Name = "sudada" middleStu.age = 18 fmt.Println(middleStu.Name) // 返回值:sudada fmt.Println(middleStu.age) // 返回值:18 }
3.当结构体和和"父"结构体有相同的字段或者方法时,编译器采用就近访问的原则,如果希望访问"父"结构体的字段或方法,就指明"父"结构体的字段或方法来取值;
package main import "fmt" // "父"结构体 type Student struct { Name string age int } // 结构体Student的方法 func (st *Student) StudentSayName() { fmt.Println(st.Name) } // 结构体School 继承"父"结构体Student type School struct { Student // 继承"结构体Student" Name string } // 结构体School的方法 func (sc *School) SchoolSayName() { fmt.Println(sc.Name) } func main() { // 定义一个结构体变量"middleStu" middleStu := School{} // 定义结构体School的"Name字段"的值为"sudada" middleStu.Name = "sudada" // 调用结构体School的方法"SchoolSayName"就能拿到School的Name字段的值 middleStu.SchoolSayName() // 返回值:"sudada" // 调用结构体Student的方法"StudentSayName"拿到Student的Name字段的值为空 middleStu.StudentSayName() // 返回值:"空" // 定义结构体Student的"Name字段"的值为"sudada" middleStu.Student.Name = "wangdalu" // 调用结构体School的方法"SchoolSayName"就能拿到School的Name字段的值 middleStu.SchoolSayName() // 返回值:"sudada" // 调用结构体Student的方法"StudentSayName"就能拿到Student的Name字段的值 middleStu.StudentSayName() // 返回值:"wangdalu" }
4.结构体继承了多个"父"结构体,同时多个"父"结构体有相同的字段或方法,那么在访问时,就需要指定"父"结构体的名字,否则编译报错;
package main // "父"结构体1(有Name字段) type A struct { Name string Age int } // "父"结构体2(也有Name字段) type B struct { Name string } // 结构体C,继承A,B结构体(没有Name字段) type C struct { A B } func main() { c := C{} // 结构体变量c给Name字段赋值时,正确的写法(必须指定"父"结构体的名称) c.A.Name="sudada" c.B.Name="sudada" // 结构体变量c给Name字段赋值,错误的写法(没有指定"父"结构体的名称) //c.Name="sudada" }
5."结构体B,字段aaa"的类型是一个"结构体A"时,这种模式就是组合(A和B组合)。组合模式下,访问"结构体A的字段或方法",必须带上"结构体B的字段名aaa";
package main // 结构体A type A struct { Name string Age int } // 结构体B type B struct { // "字段aaa"的类型是结构体A aaa A } func main() { b := B{} // 组合模式下,想要访问"A结构体"的字段或方法,必须带上"结构体B的字段名aaa" b.aaa.Name="sudada" // 错误写法 //b.Name="sudada" }
6.结构体继承"父"结构体后,在创建结构体变量时,可以直接给"父"结构体的字段赋值;
// 继承结构体 import "fmt" // 结构体A type A struct { Name string Age int } // 结构体B(继承A结构体) type B struct { A } func main() { // 结构体变量赋值,B结构体继承了A结构体,赋值方式: B{ A{Name:"sudada",Age:18} } b := B{ A{ Name: "sudada", Age: 18, }, } fmt.Println(b.Name) // 返回值:sudada fmt.Println(b.Age) // 返回值:18 // 错误写法(B结构体虽然继承了A结构体,但是B结构体没有字段Name和Age) //c := B{ // Name: "sudada", // Age: 18, //} } // 继承的结构体是一个指针 import "fmt" // 结构体A type A struct { Name string Age int } // 结构体B(继承A结构体,是一个指针) type B struct { *A } func main() { // 结构体变量赋值,B结构体继承了A结构体,赋值方式: B{ A{Name:"sudada",Age:18} } b := B{ // 这里也要适配为指针 &A{ Name: "sudada", Age: 18, }, } fmt.Println(b.Name) // 返回值:sudada fmt.Println(b.Age) // 返回值:18 fmt.Println(b) // 返回值:{ 0xc000094030 } 是一个指针地址 }
7.结构体的匿名字段是基本数据类型时,如何使用
import "fmt" type A struct { Name string Age int } // 结构体的匿名字段是基本数据类型时,如何使用 type B struct { A int } func main() { b := B{} b.Name="sudada" b.Age=18 // 结构体变量给基本数据类型赋值,以及使用 b.int=22 fmt.Println(b) // 返回值:{{sudada 18} 22} }
5.2.4、多重继承(建议少用)
什么是多重继承:一个结构体继承了多个"父"结构体,那么该结构体可以直接访问"父"结构体的字段和方法,实现了多重继承
案例:
package main // "父"结构体1 type A struct { Name string Age int } // "父"结构体2 type B struct { Name string } // 结构体C,继承A,B结构体 type C struct { A B }
5.3、多态
5.3.1、多态的基本介绍
golang中,多态特征是通过接口实现的。按照统一的接口调用不同的实现,这时接口变量就呈现不同的形态。
5.3.2、快速入门案例(复用了6.1.3的案例)
package main import "fmt" // 定义一个接口 type Usb interface { // 接口内定义2个方法 Start() Stop() } // 定义结构体Phone type Phone struct { } // 结构体Phone的Start方法 func (p Phone) Start() { fmt.Println("phone start") } // 结构体Phone的Stop方法 func (p Phone) Stop() { fmt.Println("phone Stop") } // 定义结构体Camera type Camera struct { } // 结构体Camera的Start方法 func (c Camera) Start() { fmt.Println("camera start") } // 结构体Camera的Stop方法 func (c Camera) Stop() { fmt.Println("camera Stop") } // 定义结构体Computer type Computer struct { } // 结构体Computer的Working方法,"接收参数"的类型为Usb(interface) // Usb内有2个方法"Start"和"Stop":只要结构体也绑定了"Start"和"Stop"方法,那么这个结构体实现了"Usb接口"。 func (computer Computer) Working(usb Usb) { // 这里的usb就是一种"多态"。usb的值就是传递过来的"结构体变量",会去判断导致是camera还是phone。 // 通过传递过来的"结构体变量"调用方法 usb.Start() usb.Stop() } func main() { // 结构体变量 computer := Computer{} camera := Camera{} phone := Phone{} // 调用方法 computer.Working(phone) // 返回值:phone start,phone stop computer.Working(camera) // 返回值:camera start,camera stop }
5.3.3、接口体现的"多态特性"
多态参数:上述入门案例,体现了多态参数(computer.Working方法,既可以接收phone变量,又可以接收camera变量,体现了Usb接口的多态)
多态数组,案例如下(在一个数组中,存放Phone结构体和Camera结构体,这两个结构体是两种不同类型)
package main import "fmt" // 定义一个接口 type Usb interface { // 接口内定义2个方法 Start() Stop() } // 定义结构体Phone type Phone struct { Name string } // 结构体Phone的Start方法 func (p Phone) Start() { fmt.Println("phone start") } // 结构体Phone的Stop方法 func (p Phone) Stop() { fmt.Println("phone Stop") } // 定义结构体Camera type Camera struct { Name string } // 结构体Camera的Start方法 func (c Camera) Start() { fmt.Println("camera start") } // 结构体Camera的Stop方法 func (c Camera) Stop() { fmt.Println("camera Stop") } // 定义结构体Computer type Computer struct {} // 结构体Computer的Working方法,"接收参数"的类型为Usb(interface) // Usb内有2个方法"Start"和"Stop":只要结构体也绑定了"Start"和"Stop"方法,那么这个结构体实现了"Usb接口"。 func (computer Computer) Working(usb Usb) { // 这里的usb就是一种"多态"。usb的值就是传递过来的"结构体变量",会去判断导致是camera还是phone。 // 通过传递过来的"结构体变量"调用方法 usb.Start() usb.Stop() } func main() { // 定义一个Usb接口数组,可以存放Phone和Camera的结构体变量 // 可以体现出多态数组 var usbArr [2]Usb usbArr[0] = Phone{ Name: "iphone", } usbArr[1] = Camera{ Name: "jianeng", } fmt.Println(usbArr) // 返回值:[{iphone} {jianeng}] }
5.3.4、类型断言(判断一个变量是否属于某个类型)
1.什么是类型断言?
由于接口是一般类型,不知道具体类型,如果要转成具体类型,需要使用类型断言(在进行类型断言时,如果类型不匹配,就会报错)
案例1:
package main import "fmt" type Point struct { Name string } func main() { var a interface{} var point Point = Point{Name: "x"} a = point // 如何将a赋值给b变量 var b Point // b = a 这样赋值会报错 b = a.(Point) // 类型断言 fmt.Println(b) // b = a.(Point)就是类型断言,表示判断变量a的值是否属于'Point结构体类型', 如果是则转成'Point结构体的变量'并赋值给b,否则报错。 }
案例2:
package main import "fmt" func main() { var x interface{} var b float32 = 1.1 x = b fmt.Println(x) // 返回值:1.1 // 方式1: // y := x.(float32) // 方式2: y := x fmt.Println(y) // 返回值:1.1 }
2.在类型断言时,附带检查机制(失败时提示错误信息,而不是panic)
package main import "fmt" func main() { var x interface{} var b float32 = 1.1 x = b fmt.Println(x) // 返回值:1.1 // 类型断言检测机制(ok是一个bool类型) //y,ok := x.(float64) //if ok { // fmt.Println("类型断言成功") //} else { // fmt.Println("类型断言失败",y) //} // if判断-简洁版 if y, ok := x.(float64); ok { fmt.Println("类型断言成功") } else { fmt.Println("类型断言失败", y) // 类型断言失败 0 } }
5.3.5、类型断言的案例1
需求:Phone结构体有一个Call方法,遍历Usb数组,如果是phone变量则调用Call方法
package main import ( "fmt" ) // 定义一个接口 type Usb interface { // 接口内定义2个方法 Start() Stop() } // 定义结构体Phone type Phone struct { Name string } // 结构体Phone的Start方法 func (p Phone) Start() { fmt.Println("phone start") } // 结构体Phone的Stop方法 func (p Phone) Stop() { fmt.Println("phone Stop") } // 结构体Phone的Call方法 func (p Phone) Call() { fmt.Println("phone Call") } // 定义结构体Camera type Camera struct { Name string } // 结构体Camera的Start方法 func (c Camera) Start() { fmt.Println("camera start") } // 结构体Camera的Stop方法 func (c Camera) Stop() { fmt.Println("camera Stop") } // 定义结构体Computer type Computer struct {} // 结构体Computer的Working方法,"接收参数"的类型为Usb(interface) // Usb内有2个方法"Start"和"Stop":只要结构体也绑定了"Start"和"Stop"方法,那么这个结构体实现了"Usb接口"。 func (computer Computer) Working(usb Usb) { // 通过参数usb(也就是Usb接口)去调用"Start"和"Stop"方法 usb.Start() usb.Stop() } func main() { var usbArr [2]Usb usbArr[0] = Phone{"iphone"} usbArr[1] = Camera{"jianeng"} // Phone结构体有一个Call方法,遍历Usb数组,如果是phone变量则调用Call方法 var computer Computer for _ ,v := range usbArr { // 使用类型断言,判断变量v的值是否属于"Phone结构体类型"如果是则调用"Phone结构体"的Call方法 if p, ok := v.(Phone); ok{ p.Call() // 返回值:phone Call } computer.Working(v) // 返回值如下: //phone start //phone Stop //camera start //camera Stop } }
5.3.6、类型断言的案例2
写一个函数,循环判断传入参数的类型
package main import "fmt" // 函数TypeJudge接收的参数是一个"空接口"(可以接收任意变量) // 用来判断一个"实参"具体的类型 func TypeJudge(item ...interface{}) { for _,v := range item { // v.(type):这里的type是一个关键字,固定写法 switch v.(type) { case bool: fmt.Println("这是一个,bool,类型,值为: ",v) case float64: fmt.Println("这是一个,float64,类型,值为: ",v) case int: fmt.Println("这是一个,int,类型,值为: ",v) case nil: fmt.Println("这是一个,nil,类型,值为: ",v) case string: fmt.Println("这是一个,string,类型,值为: ",v) case Student: fmt.Println("这是一个,Student,类型,值为: ",v) case *Student: fmt.Println("这是一个,*Student,类型,值为: ",v) default: fmt.Println("未知类型,类型,值为: ",v) } } } // 结构体Student type Student struct {} func main() { name := "sudada" TypeJudge(name) // 返回值:这是一个,string,类型,值为: sudada age := 18 TypeJudge(age) // 这是一个,int,类型,值为: 18 num := 1.8 TypeJudge(num) // 这是一个,float64,类型,值为: 1.8 status := true TypeJudge(status) // 这是一个,bool,类型,值为: true // 结构体变量类型1 student1 := Student{} TypeJudge(student1) // 这是一个,Student,类型,值为: {} // 结构体变量类型2 student2 := &Student{} TypeJudge(student2) // 这是一个,*Student,类型,值为: &{} }
六、面向对象-接口(interface)非常重要★★★★★★
6.1、接口的介绍和快速入门
6.1.1、接口语法
type 接口名 interface {
方法1(参数名 类型) (返回值名 返回值类型)
方法2(参数名 类型) (返回值名 返回值类型)
}6.1.2、接口介绍
1.接口可以定义多个方法,但是不需要具体实现。(只有名称没有代码逻辑),高聚合,松耦合。
2.接口不能包含任何变量。
3.结构体在使用接口时,编写接口同名方法的具体实现逻辑。只要结构体包含了接口所有的方法,那么结构体就实现了这个接口。( 结构体的方法名和接口的方法名一致时,结构体就实现了接口。案例如下:)
package main import "fmt" // 定义一个接口 type Usb interface { // 接口内定义2个方法 Start() Stop() } // 定义结构体Phone type Phone struct { } // 结构体Phone的Start方法 func (p Phone) Start() { fmt.Println("phone start") } // 结构体Phone的Stop方法 func (p Phone) Stop() { fmt.Println("phone Stop") } // 那么"结构体Phone"就实现了"接口Usb"
6.1.3、接口快速入门案例
package main import "fmt" // 定义一个接口 type Usb interface { // 接口内定义2个方法 Start() Stop() } // 定义结构体Phone type Phone struct { } // 结构体Phone的Start方法 func (p Phone) Start() { fmt.Println("phone start") } // 结构体Phone的Stop方法 func (p Phone) Stop() { fmt.Println("phone Stop") } // 定义结构体Camera type Camera struct { } // 结构体Camera的Start方法 func (c Camera) Start() { fmt.Println("camera start") } // 结构体Camera的Stop方法 func (c Camera) Stop() { fmt.Println("camera Stop") } // 定义结构体Computer type Computer struct { } // 结构体Computer的Working方法,"接收参数"的类型为Usb(interface) // Usb内有2个方法"Start"和"Stop":只要结构体也绑定了"Start"和"Stop"方法,那么这个结构体实现了"Usb接口"。 func (computer Computer) Working(usb Usb) { // 这里的usb就是一种"多态"。usb的值就是传递过来的"结构体变量",会去判断导致是camera还是phone。 // 通过传递过来的"结构体变量"调用方法 usb.Start() usb.Stop() } func main() { // 结构体变量 computer := Computer{} camera := Camera{} phone := Phone{} // 调用方法 computer.Working(phone) // 返回值:phone start,phone stop computer.Working(camera) // 返回值:camera start,camera stop }
6.2、接口的注意事项
6.2.1、接口本身不能创建实例,但是可以指向一个已经实现了该接口的变量。
package main import "fmt" // 接口A,有一个Say方法 type A interface { Say() } // 结构体Student实现了A接口 type Student struct { Name string } func (stu Student) Say() { fmt.Println(stu.Name) } func main() { // Student结构体变量stu var stu = Student{Name: "sudada"} // 结构体Student只有实现了A接口,才可以这么赋值 var a A = stu a.Say() // 返回值:sudada }
6.2.2、接口中所有的方法,只有名称,没有具体代码实现过程。
6.2.3、"结构体"或"自定义类型",需要将一个接口内的所有方法都实现,才能叫做实现了该接口。
6.2.4、只需要是"自定义的数据类型",都可以实现接口。不仅仅是"结构体类型"。
6.2.5、自定义类型可以实现多个接口(多个接口内可以有同名的方法)
package main import "fmt" // 接口A,有一个Say方法 type A interface { Say() } // 接口B,有一个Hello方法 type B interface { Hello() } // 结构体Student实现了A接口和B接口 type Student struct { Name string } func (stu Student) Say() { fmt.Println("Say") } func (stu Student) Hello() { fmt.Println("Hello") } func main() { var stu = Student{} // 调用A接口的方法Say stu.Say() // 返回值:Say // 调用B接口的方法Hello stu.Hello() // 返回值:Hello }
6.2.6、Golang接口中不能有任何变量。
6.2.7、一个"接口A"如果继承了"B,C接口",那么要"实现A接口",就必须将B,C接口全部实现。
package main import "fmt" // 接口B,有一个Hello方法 type B interface { Hello() } // 接口C,有一个Hai方法 type C interface { Hai() } // 接口A,有一个Say方法,同时继承B,C接口 type A interface { B C Say() } // 结构体Student实现了A接口(同时必须实现B,C接口) type Student struct { Name string } func (stu Student) Say() { fmt.Println("Say") } func (stu Student) Hai() { fmt.Println("Hai") } func (stu Student) Hello() { fmt.Println("Hello") } func main() { var stu = Student{} // 只有结构体Student将A接口(继承B,C接口)的所有方法都实现,才能这么使用 var a A = stu a.Say() // 返回值:Say a.Hai() // 返回值:Hai a.Hello() // 返回值:Hello }
6.2.8、接口"interface"默认是一个指针(引用类型),如果没有对接口"interface"初始化就使用,默认会返回nil(详见6.1.3案例)
6.2.9、空接口"interface{}"没有任何的方法,所以所有类型都实现了空接口"interface{}"(可以把任何一个变量赋值给空接口"interface{}"),空接口"interface{}"也是一种类型。
import "fmt" // 接口A type A interface { } // 结构体Student type Student struct { Name string } func main() { // "空接口"的使用方式1 var stu = Student{} // 接口A没有任何方法(空接口),那么就可以把任意变量赋值给空接口 var a A = stu fmt.Println(a) // 返回值:{} // "空接口"的使用方式2 // 变量test是一个"空接口"类型,赋值为"test" var test interface{} = "test" fmt.Println(test) // 返回值:test // 变量test2是一个"空接口"类型,赋值为变量num var num int = 122 var test2 interface{} = num fmt.Println(test2) // 返回值:122 }
6.3、接口的案例(对"切片"的内容进行排序)
package main import ( "fmt" "math/rand" "sort" ) // 定义结构体Hero type Hero struct { Name string Age int } // 定义结构体切片HeroSlice type HeroSlice []Hero // 结构体Hero实现接口(绑定接口的方法) // Len方法,返回结构体的长度 func (hs HeroSlice) Len() int { return len(hs) } // Less方法,决定使用怎样的方式进行排序 func (hs HeroSlice) Less(i,j int) bool { // 从小到大排序Age return hs[i].Age < hs[j].Age // 从大到小排序Age // return hs[i].Age > hs[j].Age } // Swap方法 func (hs HeroSlice) Swap(i,j int) { // a,b 两个变量交换值(a=b,b=a),方法1 //temp := hs[i] //hs[i] = hs[j] //hs[j] = temp // a,b 两个变量交换值(a=b,b=a),方法2 hs[i],hs[j] = hs[j],hs[i] } func main() { // 定义一个切片 var test = []int{0,10,20,90,40} // 对切片排序,方式2(sort排序) sort.Ints(test) fmt.Println(test) // 返回值:[0 10 20 40 90] // "结构体切片"进行排序 // 定义结构体变量heros var heros HeroSlice // 生成一些随机值h for i:=0;i<=10;i++{ h := Hero{ Name: fmt.Sprintf("英雄~%d",rand.Intn(100)), Age: rand.Intn(100), } // 将h的值append到结构体变量heros中去 heros = append(heros, h) } fmt.Println(heros) // 返回值:[{英雄~13 25} {英雄~33 47} {英雄~64 31} {英雄~29 73} {英雄~67 2} {英雄~90 50} {英雄~41 48} {英雄~50 48} {英雄~18 5} {英雄~70 95} {英雄~88 33}] // 排序前heros的顺序 for _,v := range heros { fmt.Println("-------排序前-------") fmt.Println(v) } // 调用sort.Sort进行排序 sort.Sort(heros) fmt.Println("-------按照年龄排序后-------") for _,v := range heros { fmt.Println(v) } }
七、接口和继承的比较
7.1、继承和接口解决的问题不同
1、继承主要解决代码的复用性和可维护性。
2、接口主要在于设计好各种规范(方法),让其他自定义类型去实现这些方法。
3、接口比继承更灵活,同时一定程度实现代码解耦。
7.2、简单的小案例说明
1、B结构体继承A结构体时,就自动继承了A结构体的字段和方法,并且可以直接使用。
2、B结构体需要扩展功能同时又不希望去修改继承关系时(不改变A结构体的字段和方法),则使用接口(实现接口,是对继承机制的一种补充)
package main import "fmt" // 结构体Monkey type Monkey struct { Name string } // 结构体Monkey绑定的方法 func (m Monkey) pashu() { fmt.Println(m.Name,"继承了爬树的技能...") } // 结构体LittleMonkey继承Monkey type LittleMonkey struct { Monkey } // 接口 type Fly interface { fly() } type Swim interface { swimming() } // 结构体LittleMonkey实现接口Fly func (m LittleMonkey) fly() { fmt.Println(m.Name,"学会了飞行...") } // 结构体LittleMonkey实现接口Swim func (m LittleMonkey) swimming() { fmt.Println(m.Name,"学会了游泳...") } func main() { ddd := LittleMonkey{ Monkey{Name: "悟空"}, } ddd.pashu() // 返回值:悟空 继承了爬树的技能... ddd.fly() // 返回值:悟空 学会了飞行... ddd.swimming() // 返回值:悟空 学会了游泳... }
七、面向对象-例子
7.1、例子1:写一个方法,输出对象的所有信息
package main import "fmt" type Student struct { Name string gender string age int id int score float64 } func (stu *Student)say() string { stuInfo := fmt.Sprintf( "%v\n%v\n%v\n%v\n%v", stu.Name, stu.gender, stu.age, stu.id, stu.score, ) return stuInfo } func main() { var stu Student = Student{ Name: "sudada", gender: "male", age: 18, id: 01, score: 100, } fmt.Println(stu.say()) // sudada // male // 18 // 1 // 100 }