Golang 语言面向对象编程说明
1、Golang 也支持面向对象编程(OOP),但是和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以我们说 Golang 支持面向对象编程特性是比较准确的。
2、Golang 没有类(class),Go 语言的结构体(struct)和其它编程语言的类(class)有同等的地位,可以理解 Golang 是基于 struct 来实现 OOP 特性的。
3、Golang 面向对象编程非常简洁,去掉了传统 OOP 语言的继承、方法重载、构造函数和析构函数、隐藏的 this 指针等等
4、Golang 仍然有面向对象编程的继承,封装和多态的特性,只是实现的方式和其它 OOP 语言不一样,比如继承 :Golang 没有 extends 关键字,继承是通过匿名字段来实现。
5、Golang 面向对象(OOP)很优雅,OOP 本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活。在 Golang 中面向接口编程是非常重要的特性。
结构体与结构体变量(实例/对象)的关系示意图对上图的说明:
1、将一类事物的特性提取出来(比如猫类), 形成一个新的数据类型, 就是一个结构体。
2、通过这个结构体,我们可以创建多个变量(实例/对象)
3、事物可以猫类,也可以是 Person , Fish 或是某个工具类。。。
快速入门-面向对象的方式(struct)解决养猫问题代码如下:
package main import "fmt" // 定义一个Cat结构体,将Cat的各个字段/属性信息,放入到Cat结构体中进行管理 type Cat struct { Name string Age int Color string Hobby string } func main() { // 创建一个Cat的变量 var cat1 Cat cat1.Name = "binglecat" cat1.Age = 18 cat1.Color = "yellow" cat1.Hobby = "eat and sleep" fmt.Println("cat1 = ", cat1) fmt.Println("小猫信息如下:") fmt.Println("name is ", cat1.Name) fmt.Println("age is ", cat1.Age) fmt.Println("color is ", cat1.Color) fmt.Println("hobby is ", cat1.Hobby) }
结构体和结构体变量(实例)的区别和联系
通过上面的案例我们可以看出: 1、结构体是自定义的数据类型,代表一类事物. 2、结构体变量(实例)是具体的,实际的,代表一个具体变量 结构体变量(实例)在内存的布局
如何声明结构体
基本语法:
type 结构体名称 struct { field1 type field2 type }
举例:
type Cat struct { Name string Age int Color string Hobby string }字段/属性
基本介绍
1、从概念或叫法上看: 结构体字段 = 属性 = field
2、字段是结构体的一个组成部分,一般是基本数据类型、数组,也可是引用类型。比如我们前面定义猫结构体 的 Name string 就是属性
注意事项和细节说明 1、字段声明语法同变量,示例:字段名 字段类型 2、字段的类型可以为:基本类型、数组或引用类型 3、在创建一个结构体变量后,如果没有给字段赋值,都对应一个零值(默认值): 布尔类型是 false ,数值是 0 ,字符串是 ""。 数组类型的默认值和它的元素类型相关,比如 score [3]int 则为[0, 0, 0] 指针,slice,和 map 的零值都是 nil,即还没有分配空间。 案例演示:package main import "fmt" // 如果结构体的字段类型是: 指针,slice,和map的零值都是 nil ,即还没有分配空间 // 如果需要使用这样的字段,需要先make,才能使用. type Person struct { Name string Age int Scores [5]float64 ptr *int //指针 slice []int //切片 map1 map[string]string //map } func main() { // 定义结构体变量 var p1 Person fmt.Println(p1) if p1.ptr == nil { fmt.Println("ok1") } if p1.slice == nil { fmt.Println("ok2") } if p1.map1 == nil { fmt.Println("ok3") } //使用slice, 再次说明,一定要make p1.slice = make([]int, 10) p1.slice[0] = 100 //ok //使用map, 一定要先make p1.map1 = make(map[string]string) p1.map1["key1"] = "bingle~" fmt.Println(p1) }
4、不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改,不影响另外一个,结构体是值类型。
代码演示:package main import "fmt" type Monster struct { Name string Age int } func main() { //不同结构体变量的字段是独立,互不影响,一个结构体变量字段的更改, //不影响另外一个, 结构体是值类型 var monster1 Monster monster1.Name = "牛魔王" monster1.Age = 500 monster2 := monster1 //结构体是值类型,默认为值拷贝 monster2.Name = "青牛精" monster2.Age = 888 fmt.Println("monster1=", monster1) //monster1= {牛魔王 500} fmt.Println("monster2=", monster2) //monster2= {青牛精 888} }上述代码的内存示意图: 创建结构体变量和访问结构体字段 方式—1:直接声明
var person Person
func main() { var person Person person.Name = "bingle" person.Age = 18 fmt.Println("name is ",person.Name) fmt.Println("age is ",person.Age) }
方式—2:{}
var person Person = Person{}
func main() { person := Person{ Name: "bingle", Age: 18, } fmt.Println("name is ", person.Name) fmt.Println("age is ", person.Age) }方式 —3:&
var person *Person = new (Person)
func main() { var person *Person= new(Person) //因为person是一个指针,因此标准的给字段赋值方式 //(*person).Name = "bingle" 也可以这样写 person.Name = "bingle" //原因: go的设计者 为了程序员使用方便,底层会对 person.Name = "bingle" 进行处理 //会给 person 加上 取值运算 (*person).Name = "bingle" (*person).Name = "bingle" person.Name = "bingle" // (*person).Age = 30 person.Age = 100 fmt.Println(*person) fmt.Println("name is ", person.Name) fmt.Println("age is ", person.Age) }
方式—4:{}
var person *Person = &Person{}
func main() { //下面的语句,也可以直接给字符赋值 //var person *Person = &Person{"bingle", 60} var person *Person = &Person{} //因为person 是一个指针,因此标准的访问字段的方法 // (*person).Name = "bingle" // go的设计者为了程序员使用方便,也可以 person.Name = "bingle" // 原因和上面一样,底层会对 person.Name = "bingle" 进行处理, 会加上 (*person) (*person).Name = "bingle" person.Name = "bingle~~" (*person).Age = 88 person.Age = 10 fmt.Println("name is ", person.Name) fmt.Println("age is ", person.Age) }
说明:
1、第 3 种和第 4 种方式返回的是 结构体指针。
2、结构体指针访问字段的标准方式应该是:(*结构体指针).字段名 ,比如 (*person).Name = "bingle"
3、但 go 做了一个简化,也支持 结构体指针.字段名, 比如 person.Name = "bingle"。更加符合程序员使用的习惯,go 编译器底层 对 person.Name 做了转化 (*person).Name。
struct 类型的内存分配机制我们来看一个思考题:
定义一个Person结构体
下面一段代码输出什么内容?
package main import "fmt" type Person struct { Name string Age int } func main() { var person1 Person person1.Age = 18 person1.Name = "bingle1111" var person2 Person = person1 fmt.Println(person2.Age) person2.Name = "bingle2222" fmt.Printf("person2.Name is %v,person1.Name is %v", person2.Name, person1.Name) }
输出结果:
变量总是存在内存中的,那么结构体变量在内存中是怎样存在的?
结构体在内存中的示意图:(和上面牛魔王、青牛精一个原理)
是值拷贝,内存空间是完全独立的
看下面代码:package main import "fmt" type Person struct { Name string Age int } func main() { var person1 Person person1.Age = 18 person1.Name = "bingle1111" var person2 *Person = &person1 fmt.Println(person2.Age) person2.Name = "bingle2222" fmt.Printf("person2.Name is %v,person1.Name is %v\n", person2.Name, person1.Name) fmt.Printf("person1 的地址 = %p\n",&person1) fmt.Printf("person2 的地址 = %p\n",&person2) }上面代码对应的内存图:
看下列代码,并分析原因:
func main() { var person1 Person person1.Age = 18 person1.Name = "bingle1111" var person2 *Person = &person1 fmt.Println(*person2.Age) }
不能这样写,会报错,原因是,. 的运行优先级比 * 高
结构体使用注意事项和细节
1、结构体的所有字段在内存中是连续的
看下列代码:
package main import "fmt" // 结构体 type Point struct { x int y int } // 结构体 type Rect struct { leftUp, rightDown Point } func main() { r1 := Rect{ Point{1, 2}, Point{3, 4}, } // r1有四个int, 在内存中是连续分布 // 打印地址 fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p \n", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y) }
执行结果:
内存中的示意图:(int64 是8个字节)
package main import "fmt" // 结构体 type Point struct { x int y int } // 结构体 type Rect struct { leftUp, rightDown Point } //结构体 type Rect2 struct { leftUp, rightDown *Point } func main() { r1 := Rect{ Point{1, 2}, Point{3, 4}, } // r1有四个int, 在内存中是连续分布 // 打印地址 fmt.Printf("r1.leftUp.x 地址=%p r1.leftUp.y 地址=%p r1.rightDown.x 地址=%p r1.rightDown.y 地址=%p \n", &r1.leftUp.x, &r1.leftUp.y, &r1.rightDown.x, &r1.rightDown.y) //r2有两个 *Point类型,这个两个*Point类型的本身地址也是连续的, //但是他们指向的地址不一定是连续 r2 := Rect2{&Point{10,20}, &Point{30,40}} //打印地址 fmt.Printf("r2.leftUp 本身地址=%p r2.rightDown 本身地址=%p \n", &r2.leftUp, &r2.rightDown) //他们指向的地址不一定是连续..., 这个要看系统在运行时是如何分配 fmt.Printf("r2.leftUp 指向地址=%p r2.rightDown 指向地址=%p \n", r2.leftUp, r2.rightDown) }
r2有两个 *Point类型,这个两个*Point类型的本身地址也是连续的,但是他们指向的地址不一定是连续
2、结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类型)
看下列代码:
package main import "fmt" type A struct { Num int } type B struct { Num int } func main() { var a A var b B a = A(b) // ? 可以转换,但是有要求,就是结构体的的字段要完全一样(包括:名字、个数和类型!) fmt.Println(a, b) }
3、结构体进行 type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
package main import "fmt" type Student struct { Name string Age int } type Stu Student func main() { var stu1 Student var stu2 Stu // stu2 = stu1// 这样写是错误的,可以修改为 stu2 =Stu(stu1) stu2 =Stu(stu1) fmt.Println(stu1,stu2) }
4、struct 的每个字段上,可以写上一个 tag, 该 tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化。
序列化的使用场景:
举例:
package main import ( "encoding/json" "fmt" ) type Person struct { Name string `json:"name"` // `json:"name"` 就是 struct tag Age int `json:"age"` } func main() { // 创建一个Person变量 person := Person{ Name: "bingle", Age: 18, } // 将person变量序列化为 json格式字串 // json.Marshal 函数中使用反射 jsonStr, err := json.Marshal(person) if err != nil { fmt.Println("json处理错误,", err) } fmt.Println("jsonStr is ", string(jsonStr)) }
方法
基本介绍: 在某些情况下,我们要需要声明(定义)方法。比如 Person 结构体:除了有一些字段外(年龄,姓名..),Person 结构体还有一些行为比如:可以说话、跑步..,通过学习,还可以做算术题。这时就要用方法才能完成。 Golang 中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct。 方法的声明和调用type A struct { Num int } func (a A) test() { fmt.Println(a.Num) }对上面的语法的说明 1、func (a A) test() {} 表示 A 结构体有一方法,方法名为 test 2、(a A) 体现test 方法是和 A 类型绑定的 看下面代码:
package main import "fmt" type Person struct { Name string } func (this Person) test() { fmt.Println("person.Name is ", this.Name) } func main() { var person Person person.Name = "bingle" person.test() }
对上面的总结
1、test 方法和 Person 类型绑定
2、test 方法只能通过 Person 类型的变量来调用,而不能直接调用,也不能使用其它类型变量来调用
3、func (this Person) test() {}... this 表示哪个 Person 变量调用,这个 this 就是它的副本,这点和函数传参非常相似。
4、this 这个名字,有程序员指定,不是固定, 比如修改成 person 也是可以
方法快速入门
1、给 Person 结构体添加 speak方法,输出 xxx 是一个好人
func (this Person) speak() { fmt.Println(this.Name, "是一个好人") } func main() { var person Person person.Name = "bingle" person.speak() }
2、给 Person 结构体添加 jisuan方法,可以计算从 1+..+1000 的结果,说明方法体内可以函数一样,进行各种运算
func (this Person) jisuan() { res := 0 for i := 1; i <= 1000; i++ { res += i } fmt.Println(this.Name, "计算的结果是=", res) }
3、给 Person 结构体 jisuan2 方法,该方法可以接收一个数 n,计算从1+..+n 的结果
func (this Person) jisuan2(n int) { res := 0 for i := 1; i <= n; i++ { res += i } fmt.Println(this.Name, "计算的结果是=", res) }
5、方法的调用
func main() { var person Person person.Name = "bingle" person.test() person.speak() person.jisuan() person.jisuan2(10) }方法的调用和传参机制原理 方法的调用和传参机制和函数基本一样,不一样的地方是方法调用时,会将调用方法的变量,当做实参也传递给方法。 下面,给Person 结构体添加 getSum(n1,n2) 方法,计算两数的和,并返回结果:
func (this Person) getSum(n1, n2 int) int { return n1 + n2 } func main() { var person Person person.Name = "bingle" n1 := 10 n2 := 20 res := person.getSum(n1, n2) fmt.Println("res = ", res) }
终端结果:
说明:
1、在通过一个变量去调用方法时,其调用机制和函数一样
2、不一样的地方时,变量调用方法时,该变量本身也会作为一个参数传递到方法(如果变量是值类型,则进行值拷贝,如果变量是引用类型,则进行地质拷贝)
方法的声明(定义)func (recevier type) methodName(参数列表) (返回值列表) { 方法体 return 返回值 }
1、参数列表:表示方法输入
2、recevier type : 表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型
3、receiver type : type 可以是结构体,也可以其它的自定义类型
4、receiver : 就是 type 类型的一个变量(实例),比如 :Person 结构体 的一个变量(实例)
5、返回值列表:表示返回的值,可以多个
6、方法主体:表示为了实现某一功能代码块
7、return 语句不是必须的。 方法的注意事项和细节 1、结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式 2、如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理func (this Person) showName() { this.Name = "bingle2222" fmt.Println("name in showName is ", this.Name) } func main() { var person Person person.Name = "bingle" person.showName() fmt.Println("name in main is ", person.Name) }为了提高效率,通常我们方法和结构体的指针类型绑定
修改成如下:
// 为了提高效率,通常我们方法和结构体的指针类型绑定 func (this *Person) showName() { //因为 this 是指针,因此我们标准的访问其字段的方式是 (*this).Name // (*this).Name 等价 this.Name this.Name = "bingle2222" fmt.Println("name in showName is ", this.Name) } func main() { var person Person person.Name = "bingle" (&person).showName() // 编译器底层做了优化 (&person).showName() 等价 person.showName() // 因为编译器会自动的给加上 &c //person.showName() fmt.Println("name in main is ", person.Name) }
3、Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是 struct, 比如 int , float32 等都可以有方法(有点类似于 C# 中的扩展方法)
package main import "fmt" type integer int func (i integer) print() { fmt.Println("i=", i) } //编写一个方法,可以改变i的值 func (i *integer) change() { *i = *i + 1 } func main() { var i integer = 10 i.print() i.change() fmt.Println("i=", i) }4、方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问。 5、如果一个类型实现了 String()这个方法,那么 fmt.Println 默认会调用这个变量的 String()进行输出:
package main import "fmt" type Student struct { Name string Age int } //给*Student实现方法String() func (stu *Student) String() string { str := fmt.Sprintf("Name==[%v] Age==[%v]", stu.Name, stu.Age) return str } func main() { //定义一个Student变量 stu := Student{ Name: "tom", Age: 20, } //如果你实现了 *Student 类型的 String方法,就会自动调用 fmt.Println(&stu) }方法和函数区别 1、调用方式不一样 函数的调用方式 : 函数名(实参列表) 方法的调用方式 : 变量.方法名(实参列表) 2、对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
package main import ( "fmt" ) type Person struct { Name string } //函数 //对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然 func test01(p Person) { fmt.Println(p.Name) } func test02(p *Person) { fmt.Println(p.Name) } //对于方法(如struct的方法), //接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以 func (p Person) test03() { p.Name = "bingle03" fmt.Println("test03() =", p.Name) // bingle03 } func (p *Person) test04() { p.Name = "bingle04" fmt.Println("test04() =", p.Name) // bingle04 } func main() { p := Person{"bingle"} test01(p) test02(&p) p.test03() fmt.Println("main() p.name=", p.Name) // bingle03 (&p).test03() // 从形式上是传入地址,但是本质仍然是值拷贝 fmt.Println("main() p.name=", p.Name) // bingle03 (&p).test04() fmt.Println("main() p.name=", p.Name) // bingle04 p.test04() // 等价 (&p).test04 , 从形式上是传入值类型,但是本质仍然是地址拷贝 }
创建结构体变量时指定字段值
Golang 在创建结构体实例(变量)时,可以直接指定字段的值type Stu struct { Name string Age int }方式 1 :
func main() { //方式1 //在创建结构体变量时,就直接指定字段的值 var stu1 = Stu{"bingle1111", 18} // stu1---> 结构体数据空间 stu2 := Stu{"bingle2222~", 18} //在创建结构体变量时,把字段名和字段值写在一起, 这种写法,就不依赖字段的定义顺序. var stu3 = Stu{ Name :"bingle3333", Age : 18, } stu4 := Stu{ Age : 18, Name : "bingle4444", } fmt.Println(stu1, stu2, stu3, stu4) }
方式 2:
func main() { //方式2, 返回结构体的指针类型(!!!) var stu5 *Stu = &Stu{"bingle5555", 18} // stu5--> 地址 ---》 结构体数据[xxxx,xxx] stu6 := &Stu{"bingle6666~", 18} //在创建结构体指针变量时,把字段名和字段值写在一起, 这种写法,就不依赖字段的定义顺序. var stu7 = &Stu{ Name : "bingle7777", Age :18, } stu8 := &Stu{ Age :18, Name : "bingle8888~", } fmt.Println(*stu5, *stu6, *stu7, *stu8) // }工厂模式 Golang 的结构体没有构造函数,通常可以使用工厂模式来解决这个问题。 一个结构体的声明是这样的:
package model type Student struct { Name string ... }因为这里的 Student 的首字母 S 是大写的,如果我们想在其它包创建 Student 的实例(比如 main 包),引入 model 包后,就可以直接创建 Student 结构体的变量(实例)。但是问题来了,如果首字母是小写的,比如 是 type student struct {....} 就不不行了,怎么办---> 工厂模式来解决. 工厂模式来解决问题 使用工厂模式实现跨包创建结构体实例(变量): 如果 model 包的 结构体变量首字母大写,引入后,直接使用, 没有问题 Student.go:
package model // 定义一个结构体 type Student struct { Name string }main.go:
package main import ( "fmt" model "go_code/test/models" ) func main() { stu :=model.Student{ Name: "bingle", } fmt.Println(stu) }如果 model 包的 结构体变量首字母小写,引入后,不能直接使用, 可以工厂模式解决, 代码: person.go:
package model type person struct { Name string } // 因为student结构体首字母是小写,因此是只能在model使用 // 我们通过工厂模式来解决 func NewPerson(n string) *person { return &person{ Name : n, } }main.go:
package main import ( "fmt" model "go_code/test/models" ) func main() { person := model.NewPerson("bingle") fmt.Println(person) fmt.Println(*person) }