外贸移动商城网站建设,中国企业500强入围标准,WordPress单拦主题,企业宣传画册设计公司Go面向“对象”编程#xff08;入门——结构体与方法#xff09; 1 结构体1.1 快速入门1.2 内存解析1.3 创建结构体四种方法1.4 注意事项和使用细节 2 方法2.1 方法的声明和调用2.2 快速入门案例2.3 调用机制和传参原理2.4 注意事项和细节2.5 方法和函数区别 3 工厂模式 Gola… Go面向“对象”编程入门——结构体与方法 1 结构体1.1 快速入门1.2 内存解析1.3 创建结构体四种方法1.4 注意事项和使用细节 2 方法2.1 方法的声明和调用2.2 快速入门案例2.3 调用机制和传参原理2.4 注意事项和细节2.5 方法和函数区别 3 工厂模式 Golang语言面向对象编程说明
Golang也支持面向对象编程(OOP)但是和传统的面向对象编程有区别并不是纯粹的面向对象语言。所以我们说Golang支持面向对象编程特性是比较准确的。Golang没有类(class)Go语言的结构体(struct)和其它编程语言的类(class)有同等的地位你可以理解Gelang是基于struct来实现OOP特性的。Golang面向对象编程非常简洁去掉了传统OOP语言的方法重载、构造函数和析构函数、隐藏的this指针等等。Golang仍然有面向对象编程的继承封装和多态的特性只是实现的方式和其它OOP语言不一样比如继承:Golang没有extends 关键字继承是通过匿名字段来实现。Golang面向对象OOP很优雅OOP本身就是语言类型系统(type system)的一部分通过接口(interface)关联耦合性低也非常灵活。后面同学们会充分体会到这个特点。也就是说在Golang中面向接口编程是非常重要的特性。
1 结构体
1.1 快速入门
假设定义结构体是老师属性名字年龄学校
type Teacher struct {//变量名字大写外界可以访问这个属性Name stringAge intSchool string
}下面我们尝试赋值并打印出来
type Teacher struct {//变量名字大写外界可以访问这个属性Name stringAge intSchool string
}func main() {var tea1 Teachertea1.Name 张三tea1.Age 28tea1.School 深圳大学fmt.Println(tea1, tea1)fmt.Println(老师的信息如下)fmt.Println(name, tea1.Name)fmt.Println(age, tea1.Age)fmt.Println(school, tea1.School)
}输出结果
tea1 {张三 28 深圳大学}
老师的信息如下
name 张三
age 28
school 深圳大学通过上面的案例和讲解可以看出
结构体是自定义的数据类型代表一类事务。结构体变量实例是具体的实际的代表一个具体变量。
1.2 内存解析
在Go语言中结构体的存储是在堆上。
当我们创建一个结构体实例时它的内存将被分配在堆上。这意味着结构体的生命周期可以超出创建它的函数的生命周期。
例如当我们使用new关键字或make函数创建一个结构体实例时它将被分配在堆上。
type Person struct {Name stringAge int
}func main() {// 使用 new 关键字创建结构体实例p : new(Person)p.Name Alicep.Age 25// 使用 make 函数创建结构体实例只适用于为某些类型分配内存如 map、slice 和 channelm : make(map[string]int)m[key] 42// 结构体实例被分配在堆上可以在其他函数中继续使用anotherFunc(p)yetAnotherFunc(m)
}这是因为在Go语言中所有的变量都是通过传值而不是通过引用传递。在堆上分配结构体实例可以确保结构体数据的持久性和可用性。 Go语言中结构体的内存布局
Go语言中的结构体是一组值的集合这些值被存储在内存中的一段连续的区域。结构体的内存布局取决于结构体中的成员变量顺序和类型以及对齐方式。
结构体的对齐方式
Go语言中使用的是一种称为Packing的方式进行对齐。这种方式默认对齐到最多8字节的倍数即8字节对齐。可以通过在结构体成员变量的后面添加逗号和数字的形式手动调节对齐方式。
type Person struct {Name stringAge intHeight float64
}func main() {var p Person// 获取结构体的大小size : unsafe.Sizeof(p)fmt.Printf(结构体大小%d 字节\n, size)// 获取结构体字段的偏移量nameOffset : unsafe.Offsetof(p.Name)ageOffset : unsafe.Offsetof(p.Age)heightOffset : unsafe.Offsetof(p.Height)fmt.Printf(Name 字段的偏移量%d 字节\n, nameOffset)fmt.Printf(Age 字段的偏移量%d 字节\n, ageOffset)fmt.Printf(Height 字段的偏移量%d 字节\n, heightOffset)// 结构体的对齐方式packed : unsafe.Alignof(p)fmt.Printf(结构体的对齐方式%d 字节\n, packed)输出结果
结构体大小20 字节
Name 字段的偏移量0 字节
Age 字段的偏移量8 字节
Height 字段的偏移量12 字节
结构体的对齐方式4 字节 在这个示例中我们定义了一个Person结构体它包括名字、年龄和身高三个字段。我们通过unsafe包中的函数来获取结构体的大小、字段的偏移量以及对齐方式。结构体的大小为20字节字段的偏移量分别为0字节、8字节和12字节结构体的对齐方式为4字节。
1.3 创建结构体四种方法
基本介绍
从概念或叫法上结构体字段 属性 field字段是结构体的一个组成部分一般是基本数据类型、数组、也可以是引用数据类型。指针slice和map的零值都是nil即还没有分配空间。
方式一直接声明
案例var person Person
方式二{}
案例var person Person Person{“Tom”, 18} person : Person{“Tom”, 18}
方式三
案例var person *Person new (Person)
type Person struct {Name stringAge int
}func main() {var p *Person new(Person)// (*p).Name smith 标准写法// go设计者为了程序使用方便底层对下面这个做了优化实现了这种简单的写法// 会给 p 加上 取值运算 》 (*p).Name smith p.Name smith p.Age 18fmt.Println(p)fmt.Println(*p)
}输出结果
{smith 18}
{smith 18}方式四{}
案例var person *Person Person{}
type Person struct {Name stringAge int
}func main() {var p *Person Person{}// 标准方式(*person).Name scottp.Name scottp.Age 18fmt.Println(p)
}输出结果{scott 18}
1.4 注意事项和使用细节
结构体的所有字段在内存中是连续的指针本身的地址是连续的但是指向的地址不一定是连续的。 //结构体
type Point struct {x inty 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)
}输出结果
r1.leftUp.x 地址0x9496080 r1.leftUp.y 地址0x9496084 r1.rightDown.x 地址0x9496088 r1.rightDown.y 地址0x949608c
r2.leftUp 本身地址0x948a038 r2.rightDown 本身地址0x948a03c
r2.leftUp 指向地址0x9496068 r2.rightDown 指向地址0x94960a0结构体之间可以转换但是有要求就是结构体的字段要完全一样包括名字、类型个数 type A struct {Num int
}type B struct {Num int
}func main() {var a Avar b Ba A(b)fmt.Println(a, b)
}结构体进行type重新定义相当于取别名Golang认为是新的数据类型但是相互间可以强转。
type Student struct {Name stringAge int
}type Stu Studentfunc main() {var stu1 Studentvar stu2 Stu// stu2 stu1 // 错误系统认为这是两个不一样的类型stu2 Stu(stu1)fmt.Println(stu1, stu2)
}struct的每个字段上可以写上一个标签tag该tag可以通过反射机制获取常见的使用场景就是序列化和反序列化。 type Student struct {Name string json:name // 这里就是结构体的标签tagAge int json:age
}func main() {// 1.创建一个student变量student : Student{张三, 18}// 2.将monster变量序列化为json格式字符串jsonStr, err : json.Marshal(student) // 这里json.Marshal底层用到了反射if err ! nil {fmt.Println(jsonStr报错)}fmt.Println(jsonStr:, jsonStr)fmt.Println(string(jsonStr):, string(jsonStr))
}如果这里不加上标签生成的json格式就变成string(jsonStr): {Name:张三,Age:18} 会发现这里的Name的首字母是大写这又是不可避免因为小写就调用不了 所以这里通过标签设置一个别名底层用了反射解决这个问题~~~ 输出结果string(jsonStr): {name:张三,age:18}
2 方法
在某些情况下我们需要声明定义方法。比如Person结构体除了有一些字段外(年龄姓名…)Person结构体还有一些行为。比如可以说好跑步学习等还可以做算术题。这时就要用方法才能完成
Golang中的方法是作用在指定的数据类型上的即和指定的数据类型绑定因此自定义类型都可以有方法而不仅仅只是struct。
2.1 方法的声明和调用
type A struct {Num int
}func (a A) test() {fmt.Println(a.Num)
}对上面的语法说明
func (a A) test() {} 表示A结构体有一方法方法名为 test(a A) 体现test方法是和A类型绑定的
type Person struct {Name string
}func (p Person) test() {p.Name Tomfmt.Println(test():, p.Name)
}func main() {person : Person{Name: 张三}person.test() // 调用方法fmt.Println(main p.Name, person.Name)
}
输出结果
test(): Tom
main p.Name 张三对上面的总结 test方法和Person类型绑定 test方法只能通过Person类型的遍历来调用而不能直接调用也不嫩更实用其他类型变量来调用。 func (p Person) test() {} 这个p是它的副本进行的是值传递与函数很像。
2.2 快速入门案例 给Person结构体添加speak 方法输出 xxx是一个好人 type Person struct{Name string
}//给Person结构体添加speak 方法,输出 xxx是一个好人
func (p Person) speak() {fmt.Println(p.Name, 是一个goodman~)
}给Person结构体添加jisuan 方法,可以计算从 1…1000的结果, type Person struct {Name string
}func (p Person) jisuan() {res : 0for i : 1; i 1000; i {res i}fmt.Println(p.Name, 计算的结果是, res)
}func main() {p : Person{Name: 张三}p.jisuan() // 输出张三 计算的结果是 500500
}给Person结构体jisuan2 方法,该方法可以接收一个参数n计算从 1…n 的结果 type Person struct {Name string
}// 给Person结构体jisuan2 方法,该方法可以接收一个参数n计算从 1..n 的结果
func (p Person) jisuan2(n int) {res : 0for i : 1; i n; i {res i}fmt.Println(p.Name, 计算的结果是, res)
}func main() {p : Person{Name: 张三}p.jisuan2(10) // 输出张三 计算的结果是 55
}给Person结构体添加getSum方法,可以计算两个数的和并返回结果 type Person struct {Name string
}// 给Person结构体添加getSum方法,可以计算两个数的和并返回结果
func (p Person) getSum(n1 int, n2 int) int {return n1 n2
}func main() {p : Person{Name: 张三}sum : p.getSum(1, 1)fmt.Printf(%v sum%v, p.Name, sum) //输出张三 sum2
}2.3 调用机制和传参原理
说明方法的调用和传参机制和函数基本一样不一样的地方是方法调用时会将调用方法的变量当作实参也传递给方法。下面举例说明
案例 type Person struct {Name string
}// 给Person结构体添加getSum方法,可以计算两个数的和并返回结果
func (p Person) getSum(n1 int, n2 int) int {return n1 n2
}func main() {p : Person{Name: 张三}n1 : 10n2 : 20res : p.getSum(n1, n2)fmt.Println(res, res)
}在通过一个变量去调用方法时其调用机制和函数一样不一样的地方是变量调用方法时该变量本身也会作为一个参数传递到方法如果变量是值类型则进行值拷贝如果是引用类型则进行地质拷贝
2.4 注意事项和细节
结构体类型是值类型在方法调用中遵守值类型的传递机制是值拷贝传递方式如果程序员希望在方法中修改结构体变量的值可以通过结构体指针的方式来处理Golang中的方法作用在指定的数据类型上的(即: 和指定的数据类型绑定),因此自定义类型都可以有方法而不仅仅是struct比如int ,float32等都可以有方法方法的访问范围控制的规则和函数一样。方法名首字母小写只能在本包4访问方法首字母大写可以在本包和其它包访问。如果一个类型实现了String这个方法那么fmt.Println默认会调用这个变量的String()进行输出
上面这些注意事项都比较简单就代码展示一下最后一条
type Student struct {Name stringAge 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)fmt.Println(stu)
}输出结果
{tom 20}
Name[tom] Age[20]2.5 方法和函数区别
调用方式不一样
函数的调用方式函数名实参列表方法的调用方式变量.方法名实参列表
数据传递的限制不一样
对于普通函数接收者为值类型时不能将指针类型的数据直接传递反之亦然。对于方法如struct的方法接收者为值类型时可以直接用指针类型的遍历调用方法反过来同样也可以。
这一点很容易迷糊下面用段代码解释一下
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 jackfmt.Println(test03() , p.Name) // jack
}func (p *Person) test04() {p.Name maryfmt.Println(test03() , p.Name) // mary
}func main() {p : Person{tom}test01(p)test02(p)p.test03()fmt.Println(main() p.name, p.Name) // tom(p).test03() // 从形式上是传入地址但是本质仍然是值拷贝fmt.Println(main() p.name, p.Name) // tom(p).test04()fmt.Println(main() p.name, p.Name) // maryp.test04() // 等价 (p).test04 , 从形式上是传入值类型但是本质仍然是地址拷贝
}输出结果
tom
tom
test03() jack
main() p.name tom
test03() jack
main() p.name tom
test03() mary
main() p.name mary
test03() mary从代码会发现仅管传递的是一个地址但是编译器进行了内部优化实际上还是值传递只是支持这种写法但并不是进行一个地址值的修改。
3 工厂模式
说明Golang的结构体没有构造函数通常可以使用工厂模式来解决这个问题
相当于这个工厂模式就是以前构造函数的功能。
看个需求
一个结构体的声明如下:
package modeltype Student struct {Name string
}因为这里的 Student 的首字母S是大写的如果我们想在其他包串接Student的实例比如main包引入model包后就可以直接创建Student结构体的变量实例。
但是问题来了如果搜字母是小写的比如是 type student struct {…}就不行了咋办-》工厂模式来解决
Model包
package modeltype student struct {name stringscore float64
}// 因为student结构体首字母是小写因此是只能在model使用
// 通过工厂模式来解决
func NewStudent(n string, s float64) *student {return student{name: n,score: s,}
}// 如果score字段首字母小写则在其他包不可以直接方法我们可以提供一个方法
func (s *student) GetScore() float64 {return s.score
}// 如果score字段首字母小写则在其他包不可以直接方法我们可以提供一个方法
func (n *student) GetName() string {return n.name
}
main包
import (GoStudy_Day1/modelfmt
)func main() {// 定student结构体是首字母小写我们可以通过工厂模式来解决stu : model.NewStudent(Tom, 88.8)fmt.Println(stu)fmt.Println(*stu)// fmt.Println(name, stu.name) // 报错因为是私密的fmt.Println(name, stu.GetName())fmt.Println(score, stu.GetScore())
}输出结果
{Tom 88.8}
{Tom 88.8}
name Tom
score 88.8其实这就看出来就是Java的GetSet绕了一圈又回来啦~~~