结构体struct
https://book.itsfun.top/gopl-zh/ch4/ch4-04.html
在Go中,没有传统意义上的对象的概念。一个结构体就是一个对象,结构体中包含了不同的数据类型组成一个对象。结构体属于值类型,遵循值拷贝。
field
- field的类型可以为基本类型、数组或引用类型;
- 创建出来的结构体默认有零值,即
- 布尔类型为false,数值类型为0,字符串类型为""
- 数组类型的默认值和它的元素类型相关,如score[3]int则为[0, 0, 0]
- 指针、slice和map的零值为nil,即还未分配空间
- 不同结构体变量的字段相互独立,因为结构体属于值类型
type Cat struct{
Name string
Age int
Color string
Array [5]string
Ptr *int
Slice []int // 默认为nil
Map1 map[string]string // 默认为nil
}
func main() {
var cat1 Cat
fmt.Println(cat1) // 输出'{ 0 [ ] <nil> [] map[]}'
cat1.Slice = make([]int, 10) // slice和map在使用前需创建内存空间
cat1.Slice[0] = 20
cat1.Slice = append(cat1.Slice,100)
fmt.Println(cat1) // 输出'{ 0 [ ] <nil> [20 0 0 0 0 0 0 0 0 0 100] map[]}'
}
创建结构体变量和访问结构体字段
var cat1 Cat // [推荐]方式一
cat1.Color = "白色"
cat2 := Cat{"mar",20,"白色"} // 方式二
var cat3 *Cat = new(Cat) // 方式三
car3 := new(Cat) // 方式三
cat3.Name = "小王" // 等价于(*cat3).Name = "小王",实际底层还是指针
var cat4 *Cat = &Cat{"豪锐",10,"黑色"} // 方式四
cat5 := &Cat{"豪锐",10,"黑色"} // 方式五
fmt.Println(cat1) // 输出'{ 0 白色}'
fmt.Println(cat2) // 输出'{mar 20 白色}'
fmt.Println(*cat3) // 输出'{小王 0 }'
fmt.Println(*cat4) // 输出'{豪锐 10 黑色}'
fmt.Println(*cat5) // 输出'{豪锐 10 黑色}'
// 方式三和方式四返回的是结构体指针
注意事项及使用细节
- 结构体的所有字段在内存中是连续分布的;结构体的字段虽然是连续分布的,但是如果是取的是取值得值,就不一定是连续的了;
type Rect struct{
leftUp,rightDown Ponit
}
type Rect2 struct{
leftUp,rightDown *Ponit
}
func main() {
r1 := Rect{Ponit{1,2}, Ponit{3,4}}
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)
// 输出'r1.leftUp.x=0xc0000101a0,r1.leftUp.y=0xc0000101a8,r1.rightDown.x=0xc0000101b0,r1.rightDown.y=0xc0000101b8'
}
- 结构体是用户单独定义的类型,和其他类型进行转换时需要有完全相同的字段(名称、个数和类型);
- 结构体进行type重新定义(相当于取别名),Go认为是新的数据类型,但是互相间可以强转;
type interger int // Go
func main(){
var i interger = 10
var j int = 20
i = interger(j)
fmt.Println(i,j) // 输出'20 20'
}
- 在struct的每个字段上,可以添加tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化;
package main
import (
"fmt"
"encoding/json"
)
type Cat struct{
Name string `json:"name"` // tag用于序列化
Age int `json:"age"`
Color string `json:"color"`
}
func main() {
var cat4 *Cat = &Cat{"豪锐",10,"黑色"}
jsonCat, err := json.Marshal(*cat4)
if err != nil {
fmt.Println("json处理错误",err)
}
fmt.Println(string(jsonCat)) // 输出'{"name":"豪锐","age":10,"color":"黑色"}'
方法
当我们为结构体嵌入一个匿名结构体时,该结构体不仅会获得匿名成员类型的所有成员,而且也获得该结构体导出的全部方法。该机制可以用于将拥有一些简单行为的对象组合成有复杂行为的对象。这是Go中面向对象编程的核心。
声明及定义
func ([recevier] [type]) [method_name]([参数列表]) ([返回值列表]) { // [recevier]推荐用this,且调用方法时要注意会附带结构体的变量
[方法体]
return [返回值]
}
type Person struct {
Name string
}
func (a Person) test() { // 1.该方法与Person绑定 ,a可以随便定义类似于Python的self
// 2.test方法和Person类型绑定,只能通过Person类型的变量来调用
fmt.Println("test() name=",a.Name) }
// a表示哪个Person变量调用,这个a就是它的副本
func main() {
var c Person // 定义c为Person类型,能调用Person的field和方法
c.Name = "tom" // 修改field
c.test() // 调用方法
}
// 输出'test() name= tom'
调用和传参机制原理
与函数的区别在于,方法调用时,会将调用方法的变量,当作实参也传递给方法。
type Circle struct {
redius float64
}
func (circle Circle) area() (float64) {
var result float64
result = 3.14 * circle.redius * circle.redius
return result
}
func main() {
var c Circle
c.redius = 4.0 // 此时main栈的值为4.0
res := c.area() // 运行到area()方法时,将c.redius值拷贝一份给area()栈的circle。所以area()栈的circle.redius也等于4.0
fmt.Println(res) // 输出'50.24'
}
注意事项和细节
- 在方法调用中,遵守值类型的传递机制,是值拷贝传递。可以通过结构体指针的方式(效率高)来修改结构体变量的值;
- 方法作用在指定的数据类型上(即:和指定的数据类型绑定),因此自定义类型(struct、int、float32等)都可以有方法;
- 遵循函数的访问范围控制规则,即大小写访问限制;
- 如果一个变量实现了
String()
这个方法,那么fmt.Println()
默认会调用这个变量的String()
进行输出;
type Students struct {
Name string
Age int
}
func (stu *Students ) String() (string) { // String方法重写
str := fmt.Sprintf("Name=[%v],Age=[%v]",stu.Name,stu.Age)
return str
}
func main() {
tom := Students{
Name : "tom",
Age : 11,
}
fmt.Println(&tom) // 输出'Name=[tom],Age=[11]',若不重写String则输出'&{tom 11}'
}
与函数的区别
- 调用方式不一样。函数通过[函数名]([实参列表]);方法通过[变量].[方法名]([实参列表]);
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然;
- 对于方法(如struct方法),接收者为值类型时,可以直接用指针类型的变量调用方法(但仍遵循值拷贝),反之亦然;
- 真正决定是值拷贝还是地址拷贝,看这个方法和哪些类型绑定。如(p Person)为值拷贝,(p *Person)为地址拷贝;
二叉树案例
package main
import (
"fmt"
)
type tree struct {
value int
left, right *tree
}
// Sort sorts values in place.
func Sort(values []int) {
var root *tree
for _, v := range values {
root = add(root, v)
}
fmt.Println("1",values)
fmt.Println("root=",root)
appendValues(values[:0], root)
}
// appendValues appends the elements of t to values in order
// and returns the resulting slice.
func appendValues(values []int, t *tree) []int {
if t != nil {
fmt.Println("t.left=",t.left)
values = appendValues(values, t.left)
fmt.Println("t.value=",t.value)
values = append(values, t.value)
fmt.Println("t.right=",t.right)
values = appendValues(values, t.right)
fmt.Println("values=",values)
}
return values
}
func add(t *tree, value int) *tree {
if t == nil {
// Equivalent to return &tree{value: value}.
t = new(tree)
t.value = value
return t
}
if value < t.value {
fmt.Println("add(t.left)=",t.left)
t.left = add(t.left, value)
} else {
fmt.Println("add(t.right)=",t.right)
t.right = add(t.right, value)
}
return t
}
func main() {
x := []int{5, 7, 1, 6, 9, 8, 2, 4, 3}
Sort(x[:])
fmt.Println(x)
}
](
面向对象编程
说明
面向对象编程是一种思想,大部分语言都能通过代码实现该思想,只不过Go中的实现方式与其他语言的实现方式有所不同而已。
- Go支持面向对象编程(OOP),但和传统的面向对象编程有区别,并不是纯粹的面向对象语言。所以说Go支持面向对象编程特性是比较准确的;
- Go中没有类(class),Go的结构体(struct)和其他编程语言的类(class)有同等地位。可以理解为Go是基于struct来实现OOP特性的;
- Go面向对象编程非常简洁,去掉了传统OOP语言的集成、方法重载(多个同名方法,根据定义顺序不同返回不同数据)、构造函数和析构函数、隐藏的this指针等等;
- 但Go仍然有面向对象编程的集成、封装和多态的特性,只是实现方式与其他OOP语言不一样。例如继承是通过匿名字段来实现的;
- Go面向对象(OOP)很优雅,OOP本身就是语言类型系统(type system)的一部分,通过接口(interface)关联,耦合性低,也非常灵活;
实例
type Student struct {
Name string `json:"name"`
Gender string `json:"gender"`
Age int `json:"age"`
Id int `json:"id"`
Score float64 `json:"score"`
}
func (student *Student) say() string {
return fmt.Sprintf("%v", *student) // 修改打印格式
}
func main() {
wang := Student{"小王","通用",11,1,90.5}
fmt.Println(wang.say())
}
工厂模式
对于私有变量或私有结构体,其他开发语言通过构造函数来获取结构体的实例,Go中没有构造函数的概念,可以通过工厂模式来解决这个问题(通过公有方法调用私有字段)。
结构体若是私有,使用工厂模式解决。结构体内的参数如果是私有,则定义一个方法(指针方法)使其返回私有方法。
三大特性
实现方式与其他OOP语言不一致,不要被语言束缚住了思想
封装(encapsulation)
封装就是将抽象出的字段和对字段的操作封装在一起,数据被保护在内部,程序的其他包只能通过被授权的操作(方法)才能对字段进行操作。类似于工厂模式,对外提供一个方法,由方法对具体的字段实现操作。封装是一种思想,工厂模式是封装的实现方式。
实现步骤
- 将结构体、字段(属性)的首字母小写
- 给结构体所在包提供一个工厂模式的函数,首字母大写,类似于构造函数
- 提供一个首字母大写的Set方法,用于对属性判断并赋值
func ([结构体名称] [结构体类型]) SetXxx([参数列表]) ([返回值列表]){
// [数据验证的业务逻辑]
[结构体名称].[字段] = [参数]
}
4 . 提供一个首字母大小的Get方法,用于获取属性的值
func ([结构体名称] [结构体类型]) GetXxx(){ return [结构体名称] }
实例
// D:\MyNutcloud\wlhiot_manage\goproject\src\go_code\object\person\main\main.go
package main
import (
"fmt"
"go_code/object/person/model"
)
func main() {
p := model.NewPerson("daihaorui")
p.SetAge(23)
fmt.Printf("%v的年龄为%v", p.Name, p.GetAge())
}
// D:\MyNutcloud\wlhiot_manage\goproject\src\go_code\object\person\model\person.go
package model
import "fmt"
type person struct {
Name string
age int
salary float64
}
func NewPerson(name string) *person {
return &person{
Name :name,
}
}
func (p *person) SetAge(age int) {
if age > 0 && age < 150 {
p.age = age
} else {
fmt.Println("年龄范围为0~150")
}
}
func (p *person) GetAge() int { return p.age }
继承(extend)
在Go中,如果一个struct嵌套了另一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现继承特性。
注意事项:
- 结构体可以使用嵌套匿名结构体中所有的字段和方法(不论是否首字母大写,仅限同包访问);
- 当调用A(被继承结构体)的方法时,会访问A的字段而不是B的;
- 结构体嵌入多个匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法)。在访问时,就必须指定匿名结构体的名称,否则编译报错;
- 当结构体和匿名结构体有相同字段或者方法时,采取就近访问原则。如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分;
- 如果struct嵌套了一个有名结构体,这种模式叫做组合,该方式下要访问组合的结构体的字段或方法必须带上有名结构体的名称;
- 任何命名的类型都可以作为结构体的匿名成员;
多态
变量(实例)具有多种形态。在Go中,多态特征是通过接口实现的。可以按照统一的接口来调用不同的实现
多态参数,在前面的案例中,computer既可以接收手机变量,也可以接收相机变量,就体现了Usb接口多态;
多态数组,在Usb数组中,存放Phone结构体和camera结构体变量。
类型断言
由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言。
func main() {
var a interface{}
var point Point = Point{1, 2}
a = point
var b Point
b = a.(Point) // 类型断言,表示判断a是否指向Point类型的变量,如果是就转成Point类型并赋给b变量,否则报错
fmt.Println(b) // 输出'{1 2}'
}
// 带检测的类型断言
func main() {
var x interface{}
var y float32 = 1.1
x = y
z, flag := x.(float64)
if flag {
fmt.Println("convert success")
} else {
fmt.Println("convert failed")
}
fmt.Printf("z的类型是%T,值=%v",z,z ) // 输出'z的类型是float32,值=1.1'
}
// 最佳实践
type Student struct {
Name string
Age int
}
func TypeJudge(items ...interface{}) {
for index, value := range items {
switch value.(type) {
case bool :
fmt.Printf("第%v个参数有是bool类型,值是%v\n",index,value)
case float32 :
fmt.Printf("第%v个参数有是float32类型,值是%v\n",index,value)
case float64 :
fmt.Printf("第%v个参数有是float64类型,值是%v\n",index,value)
case int, int32, int64 :
fmt.Printf("第%v个参数有是整数类型,值是%v\n",index,value)
case string :
fmt.Printf("第%v个参数有是string类型,值是%v\n",index,value)
case Student :
fmt.Printf("第%v个参数有是Student类型,值是%v\n",index,value)
case *Student :
fmt.Printf("第%v个参数有是%T类型,值是%v\n",index,value,value)
default:
fmt.Printf("第%v个参数不确定,值是%v\n",index,value)
}
}
}
func main() {
var n1 float32 = 1.1
var n2 float64 = 2.4
var n3 int32 = 3
var name string = "小戴"
strdent := Student{
Name : "小王",
Age : 18,
}
strdent2 := &Student{
Name : "小王",
Age : 18,
}
TypeJudge(n1,n2,n3,name,strdent,strdent2)
}
接口(interface)
interface类型可以定义一组方法,但是不需要实现。并且interface不能包含任何变量。当某个自定义类型需要使用的时候,再根据具体情况将使用这些方法。
基本语法如下:
type [接口名] interface {
[方法名1]([参数列表]) [返回值列表]
}
Go中的接口,不需要显式的实现。只要一个变量,含有接口类型中的所有方法,那么这个变量就实现了这个接口。体现了程序设计的多态和高内聚低耦合的思想。
快速入门案例
package main
import "fmt"
type Usb interface {
Start()
Stop()
}
type Phone struct {}
type Carmer struct {}
type Computer struct {}
func (p Phone) Start() {
fmt.Println("手机开始工作") }
func (p Phone) Stop() {
fmt.Println("手机结束工作") }
func (c Carmer) Start() {
fmt.Println("相机开始工作") }
func (c Carmer) Stop() {
fmt.Println("相机结束工作") }
func (computer Computer) Work(usb Usb) {
usb.Start()
usb.Stop() }
func main() {
computer := Computer{}
carmer := Carmer{}
phone := Phone{}
computer.Work(carmer)
computer.Work(phone)
}
// 输出结果"相机开始工作\n相机结束工作\n手机开始工作\n手机结束工作"
注意事项和细节
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例);
var p Phone
var stu Usb = p // Usb是一个接口
stu.Start() // 相当于调用p.Start()
- 接口中所有的方法都没有方法体;
- 在Go中,一个自定义类型需要将某个接口的全部方法都实现,才能说这个自定义类型实现了该接口;
- 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型;
- 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型;
- 一个自定义类型可以实现多个接口;
func (i integer) Say() {
fmt.Println("integer Say() = ", i) }
type AInterface interface {
Say()
}
type BInterface interface {
Hello()
}
type Monster struct{}
func (hello Monster) Hello() {
fmt.Println("Monster_hello=", hello)
}
func (say Monster) Say() {
fmt.Println("Monster_say=", say)
}
func main() {
var monster Monster
monster = Monster{}
var a2 AInterface = monster
var b2 BInterface = monster
a2.Say()
b2.Hello()
}
- Go接口中不能有任何变量;
- 一个接口(如A接口)可以继承多个别的接口(如B和C接口),这时如果要实现A接口,也必须将B和C接口的方法也全部实现(如果出现B和C拥有相同的方法,编译报错);
- interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,会报nil;
- 空接口interface{}没有任何方法,所以所有类型都实现了空接口,即可以将任何变量赋给空接口;
type T interface{} // 定义空接口
func main() {
var stu1 float64 = 404.4
var t T = stu1
fmt.Println(t) // 输出'404.4'
}
最佳实践
实现对Hero结构体切片的排序
package main
import (
"fmt"
"math/rand"
"sort" )
type Hero struct {
Name string
Age int
}
type HeroSlice []Hero
func (hs HeroSlice) Len() int {
return len(hs)
}
func (hs HeroSlice) Less(i, j int) bool {
return hs[i].Age > hs[j].Age
}
func (hs HeroSlice) Swap(i, j int) {
hs[i], hs[j] = hs[j], hs[i]
}
func main() {
var intSlice = []int{0, -1, 10, 7, 90}
sort.Ints(intSlice)
fmt.Println(intSlice)
var heroes HeroSlice
for i := 0; i < 10; i++ {
hero := Hero{
Name: fmt.Sprintf("英雄%d", rand.Intn(100)),
Age: rand.Intn(100),
}
heroes = append(heroes, hero)
}
for _, v := range heroes {
fmt.Println(v)
}
sort.Sort(heroes)
fmt.Println("------------------------------")
for _, v := range heroes {
fmt.Println(v)
}
}
接口 & 继承
当A结构体需要扩展功能,同时不希望去破坏继承关系,则可以去实现某个接口即可。
接口和继承解决的问题不同,继承的价值在于解决代码的复用性和可维护性。接口的价值在于设计好各种规范(方法),让其他自定义类型去实现这些方法;
接口在一定程度上实现代码解耦;
官方文档
接口是合约
当你看到一个接口类型的值时,并不知道它是什么,只知道可以通过它的方法来做什么
接口类型
接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例
实现接口的条件
一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口
因为ReadWriter和ReadWriteCloser包含有Writer的方法,所以任何实现了ReadWriter和ReadWriteCloser的类型必定也实现了Writer接口