Go语言不太快速的入门(3),详解向文章| 青训营笔记

151 阅读13分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第3天。

结构体

概念

结构体是自定义的数据类型,代表一类事物,拥有其自己的各种属性、方法;
结构体变量(实例)是具体的,实际的,代表一个具体变量;
结构体是一个值类型

结构体声明

type 结构体名称 struct{
   field1 type
   field2 type
}

字段的类型可以为:基本类型、数组或引用类型

在创建结构体变量后,各字段会被分配一个初值,由其数据类型决定;
不同结构体变量的字段是独立的、互不影响的,也就是说,如果把一个结构体赋给一个变量,会开辟一片新的空间,故彼此的值改变不会影响彼此(这也反映了结构体是一个值类型)。

创建实例的四种方式

1.直接根据自定义结构体类型名声明变量

var person Person

2.类型推导,并直接设置字段的值

person1 := Person{"Rose", 20}
可以以键值对的形式来直接指定字段,如name: "Rose"

3.new一个指向创建的结构体的指针

var person2 *Person = new(Person)
(*person2).Name = "Jack"
(*person2).age = 18

注意这里也可以直接:person2.Name = "jack",但上述的写法是更为严谨的
这是因为go在底层会对结构体这里的指针加上取值运算,方便开发者

4.声明结构体指针直接设置字段值

var person3 *Person = &Person{}
(*person3).Name = "Durant"
(*person3).age = 33

这里在使用(赋值)时,方法与第三张基本一致,同时也可以直接在括号中赋值

注意:第3种和第4种方式返回的是结构体指针
结构体指针标准的访问方式应该是(*结构体指针).字段名;
但也可以像其他语言一样,直接理解为对象.字段名,go底层会进行处理,是的,Go会出手!

var p1 *Person = &person1
p1.Name = "olderRose"
fmt.Println(p1.Name + "\n" + person1.Name) // person1之前定义过...
比如如上代码,p1是指向person1的指针,为引用类型,故若修改p1的字段,person1的字段值也会被修改

结构体的内存分布

结构体变量会指向内存中为第一个字段分配的地址,后续字段会被分配到匹配自己大小的空间跟随其后 当其中某个字段是个引用类型,存入的就是其所引用的地址。

结构体的所有字段在内存中是连续的,间隔的空间取决于数据类型所占的空间,对于指针,本身的地址是连续的,但指向的地址不一定是连续的

注意事项

1.结构体是用户单独定义的类型,两个结构体进行转换时需要有完全相同的字段(名称、个数与类型)

type A struct {
   num int
}

type B struct {
   num int

var a A
var b B
a = A(b)   // 简单赋值是不行的,必须要进行强转,且如果字段有不一致,则不能转换
fmt.Println(a ,b)

2.结构体进行type重新定义就相当于改了名,go认为是新的数据类型,但是相互间可以强转,这常用于用基本数据类型去定义一个自定义数据类型的情况

type Student struct{

}

type Stu Student

var stu1 Student
var stu2 Stu
stu2 == stu1   // false,且也不能赋值

3.结构体的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化,字段在命名时,首字母必须大写,否则无法序列化;
通过标签,可以使得json序列化后的字段名首字母小写

type Monster struct {
   Name string `json:"name"`
   Age int `json:"age"`
   Skill string `json:"skill"`
}

结构体方法

go中的方法是作用在指定的数据类型上的,即和指定的数据类型绑定,因此自定义类型都可以有方法,而不仅仅是struct。

基本语法

type A struct{
   Num int
}
func (a A) test(){    // 将test方法绑定给了A类型

}
在函数名前的(a A)表示结构体A有一绑定的方法,即只能通过A类型的变量来调用

比如:

func (p Person) getSum(n1 int, n2 int) int {
   return n1 + n2
}

调用与传参机制

结构体方法调用与传参机制和函数基本一样,不同的是方法调用时会把调用该方法的变量当做实参传进去,即方法栈中会自动生成一块空间给变量(会进行值拷贝或地址拷贝);
在方法中,直接使用调用方法的变量的形参名.字段获取对应的值;
这就和其他语言的面向对象很像了,可以理解为结构体传入的变量就为OOP中的对象,甚至连调用方法的格式都是一样的哦~

结构体方法格式:

func (receive type) methodName (参数列表) (返回值列表){
   方法体
   return 返回值
}

receive type:将该方法与对应的type进行绑定

注意事项

1.结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式;

2.在方法中也可以通过结构体指针来修改变量的值,实际上为了提高效率,通常方法和结构体的指针类型绑定

func (c *Circle) area() float64 {
   return 3.14 * (*c).radius * (*c).radius
   // 编译器底层做了优化,也可以直接写c.radius
}
// 以下代码在main中
var c Circle
c.radius = 5.0
res := (&c).area()
// 编译器底层做了优化,也可以直接写c.area()
fmt.Println(res)

反之亦然,即在传入的类型是指类型时,也可以通过指针的形式去操作,但由于类型已经定义了是值拷贝,不会对原字段造成影响。
这里和Java多态中在new对象时所考虑的编译类型和运行类型有些像,重点就是看传入的类型,由此判断是值拷贝还是地址拷贝。

3.也可以为int、float32等各种基本类型写方法

type integer int

func (i *integer) change() {
   *i = *i + 1
}
// 以下代码在mainvar i integer = 20
i.change()

4.方法的可访问范围与规则,与函数一样,即通过首字母大小写来控制

5.如果一个类型实现了String()这个方法,那么在输出该类型时会默认调用该方法

func (p Person) String() string {
   str := fmt.Sprintf("name=%v, age=%v", p.Name, p.age)
   return str
}

这时候大家各自的DNA应该动了叭~
类似于Python中的__str__
Java中的toString方法。

go中的面向对象

大家肯定看出来了,go中的结构体类似于其他语言中的class。

FBI Warning

1.go支持面向对象编程,但不是纯粹的面向对象语言,所以更准确的说是go支持面向对象编程特性;
2.go的结构体(struct)与其他编程语言的类(class)有同等地位,可以理解go是基于struct来实现oop特性的
3.go面向对象编程非常简洁,去掉了传统oop语言的继承、方法重载、构造函数和析构函数、隐藏的this指针等等;
4.go仍有面向对象编程的继承、封装和多态特性,只是实现方式和其他语言不太一样,比如继承:go中没有extends关键字,继承是通过匿名字段来实现的;
5.oop本身就是语言类型系统(type system)的一部分,通过接口(interface关联),耦合性低,也非常灵活,也就是说在go中面向接口编程时非常重要的特性。

工厂模式

工厂模式在go中相当于是其他语言的构造函数
可以用于解决结构体的首字母为小写,而让其他包可以访问到该结构体的情况,进而实现封装特性。

type student struct {
   Name  string
   Score float64
}

// 如果结构体首字母小写,可以通过工厂模式解决

func NewStudent(n string, s float64) *student {
   return &student{
      Name: n,
      Score: s,
   }
}

// 对于某个字段
func (s *student) GetScore() float64 {
   return s.score
}

这里就和Java中的构造函数以及Get和Set不是说很像,而简直实现的意义是一模一样。

封装

封装就是把抽象出来的字段和字段的操作封装在一起,数据被保护在内部,程序的其他包也只有通过被授权的操作(方法),才能对字段进行操作。

封装的实现

1.将结构体、字段(属性)的首字母小写(private);
2.给结构体所在的包提供一个工厂模式的函数,首字母大写,类似于一个构造函数;
3.提供一个首字母大写的Set方法,用于对属性判断并赋值

func (变量 结构体类型名) SetXxx (参数列表) (返回值列表) {
   // 加入数据验证的业务逻辑
   变量.字段 = 参数
}

4.提供一个首字母大写的Get方法,用于对属性判断并赋值

func (变量 结构体类型名) GetXxx () {
   return 变量.字段
}

继承

个人认为go的继承是很简洁直接的,直接在结构体中声明即可。

type Goods struct {
   Name string
   Price int
}

type Book struct {
   Goods  // 嵌套匿名结构体Goods,继承Goods
   writer string
}

注意事项

1.结构体可以使用嵌套匿名结构体所有的字段与方法,即首字母大写或小写的字段、方法都可以使用

type A struct {
   Name string
   age  int
}

func (a *A) SayOk() {
   fmt.Println("ok", a.Name)
}

func (a *A) sayHello() {
   fmt.Println("hello", a.Name)
}

type B struct {
   A
}

func main() {
   var b B
   b.A.Name = "Jack"
   b.A.age = 18
   b.A.SayOk()
   b.A.sayHello()
}

2.匿名结构体字段访问可以简化

// 继承关系在上段代码中
func main() {
   var b B
   b.Name = "Jack"    // 严格来说应该为b.A.name
   b.age = 18
   b.SayOk()
   b.sayHello()
}

3.当结构体和匿名结构有相同的字段或方法时,编译器采用就近访问原则,如果要访问匿名结构体的方法,可以通过匿名结构体名来区分;
如果结构体中有字段名与继承的字段中重名,那么在赋值时会赋值到结构体中的字段,继承的字段就会是空值。
此机制和其他大部分编程语言是一致的。

4.结构体嵌入两个或多个匿名结构体,若两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体的名字,就不能像事项2中那样简化了,否则编译报错,如:

c.A.name = "xxx"

5.如果一个结构体嵌套了一个有名结构体,这种模式就是组合,如果是组合关系,那么在访问组合的结构体的字段或者方法时,就必须带上结构体的名字

type C struct {
   a A       // 有名结构体
}

var c C
c.a.Name = "Rose"

Javer的DNA动起来叭~

6.嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值

type Goods struct {
   Name string
   Price float64
}

type Brand struct {
   Name string
   Address string
}

type TV struct {
   Goods
   Brand
}

tv := TV{
   Goods{"电视机", "5123.2"},   // 直接指定设置字段值
   Brand{"格力", "深圳"},
}

多态

多态即变量(实例)具有多种形态,go中的多态是通过接口实现的,可以按照统一的接口来调用不同的实现,这是接口变量就呈现不同的形态,即通过接口,就能判断出实现其的结构体

如果您之前不太了解接口,请先预览下一章节。

如一个多态数组,使得数组中可以拥有不同的类型:

var usbArr [3]Usb
usbArr[0] = Phone{"HuaWei"}
usbArr[1] = Phone{"XiaoMi"}
usbArr[2] = Camera{"suoni"}

类型断言

类型断言是在赋值时,判断要赋值的类型能否赋给被赋值的类型,比如判断能否将一个接口变量,赋给自定义类型的变量。 比如:

var t float32 = 1.1
var x interface{}  // 空接口,能接受任意类型

x = t
y := x.(float32)   // 类型断言,输出y的类型为float32

类型断言时,如果类型不匹配,会报panic,因此进行类型断言时,要确保原来的接口指向的就是断言的类型

检测类型断言:y, ok = x.(float64)  由此可以使程序不抛panic

func (p Phone) Call() {
   fmt.Println("call")
} // Camera中没有这个方法

func (c Computer) Working(usb Usb) {
   // 传入的是接口,usb可以调用实现它的结构体变量
   usb.Start()

   if phone , ok := usb.(Phone); ok == true{    // 通过判断使程序具备健壮性
      phone.Call()
   }

   usb.Stop()
}

接口

interface类型定义了一组方法,但不需要实现,和其他编程语言不相同的是,go中的interface中不能包含任何变量。

基本语法

type 接口名 interface {
   method1(参数列表) 返回值列表
   method2(参数列表) 返回值列表
   ...
}

func (t 自定义类型) method1(参数列表) 返回值列表{
   方法实现
}

比如:

type Usb interface {
   Start()
   Stop()
}

type Phone struct {

}

type Computer struct {

}

func (p Phone) Start()  {
   fmt.Println("手机开始工作")
}

func (c Computer) Working(usb Usb) {
   // 传入的是接口,usb可以调用实现它的结构体变量
   usb.Start()
   usb.Stop()
}
// 以下代码在main中
computer := Computer{}
phone := Phone{}

computer.Working(phone)       // 传入的是谁就调用谁实现的方法

注意事项

1.接口里所有的方法都没有方法体,即接口的方法都是没有实现的方法,接口体现了程序设计的多态和高内聚低耦合的思想,要注意实现接口必须要实现接口中所有的方法

2.go中的接口,不需要显式的实现(不需要明确指明实现了谁),只要一个变量,含有接口类型中所有方法,那么变量就实现了这个接口,因此,go中没有像Java中implement这样的关键字;

3.接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例),即一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型

type AInterface interface {
   Say()
}

type Stu struct {
   Name string
}

func (stu Stu) Say()  {
   fmt.Println("say")
}

var stu Stu    // 实现了接口的变量
var a AInterface = stu    // 指向了实现了该接口的变量
a.Say()   // 由此可以像一个对象一样调用方法

4.只要是自定义类型都可以实现接口,不仅仅是结构体,且一个自定义类型可以实现多个接口,注意:传入的如果是类型的指针,则不会实现接口

5.接口中不能有任何变量,这一点是和其他语言不一样的,要注重一下;

6.一个接口也可以继承其他的接口,但继承后如果要实现该接口就要把继承的接口中的所有方法也全部实现,注意:继承的接口中不要出现相同的方法名,否则编译错误

7.interface类型默认是一个指针,是引用类型,如果没有对interface初始化就使用,那么会输出nil;

8.空接口interface{}没有任何方法,所有所有类型都实现了空接口。

下面是老生常谈的几句话:

可以认为实现接口是对继承机制的补充(不破坏继承关系)
继承的价值主要在于:解决代码的复用性和可维护性,是is-a的关系
接口的价值主要在于:设计,且比继承更灵活,是like-a的关系,且一定程度上实现了代码解耦

以上内容若有不正之处,恳请您不吝指正!