包、指针、结构体和接口

包、指针、结构体和接口

指针

结构体

接口

包介绍

Go语言中支持模块化的开发理念,在Go语言中使用包(package)来支持代码模块化和代码复用。

包的引入使得我们可以去调用自己或者别人的模块代码,方便了我们的开发。

例如,在之前的课件中,我们引入了 fmt 这个包。这样使得我们可以调用 fmt 包内部的函数和变量。

1
2
3
4
5
6
7
package main

import "fmt"

func main(){
  fmt.Println("Hello world!")
}

接下来详细介绍一下

定义包

我们可以根据自己的需要创建自定义包。一个包可以简单理解为一个存放.go文件的文件夹。

该文件夹下面的所有.go文件都要在非注释的第一行添加如下声明,声明该文件归属的包。

1
package packagename

另外需要注意一个文件夹下面直接包含的文件只能归属一个包,同一个包的文件不能在多个文件夹下。

包名为main的包是应用程序的入口包,这种包编译后会得到一个可执行文件,而编译不包含main包的源代码则不会得到可执行文件。

可见性

在同一个包内部声明的标识符都位于同一个命名空间下,在不同的包内部声明的标识符就属于不同的命名空间。想要在包的外部使用包内部的标识符就需要添加包名前缀,例如fmt.Println("Hello world!")

如果想让一个包中的标识符(如变量、常量、类型、函数等)能被外部的包使用,那么标识符必须是对外可见的(public)。在Go语言中是通过标识符的首字母大/小写来控制标识符的对外可见(public)/不可见(private)的。在一个包内部只有首字母大写的标识符才是对外可见的。

例如我们定义一个名为demo的包,在其中定义了若干标识符。在另外一个包中并不是所有的标识符都能通过demo.前缀访问到,因为只有那些首字母是大写的标识符才是对外可见的。

1
2
var	Name  string // 可在包外访问的方法
var	class string // 仅限包内访问的字段

包的引入

要在当前包中使用另外一个包的内容就需要使用import关键字引入这个包,并且import语句通常放在文件的开头,package声明语句的下方。完整的引入声明语句格式如下:

1
import importname "path/to/package"

其中:

  • importname:引入的包名,通常都省略。默认值为引入包的包名。
  • path/to/package:引入包的路径名称,必须使用双引号包裹起来。
  • Go语言中禁止循环导入包 这个一定要注意,很有可能因为这个要重新架构

一个Go源码文件中可以同时引入多个包,例如:

1
2
3
import "fmt"
import "net/http"
import "os"

当然可以使用批量引入的方式。

1
2
3
4
5
import (
    "fmt"
  	"net/http"
    "os"
)

如果引入一个包的时候为其设置了一个特殊_作为包名,那么这个包的引入方式就称为匿名引入。

一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。 被匿名引入的包中的init函数将被执行并且仅执行一遍。

1
import _ "github.com/go-sql-driver/mysql"

匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。

Go语言中的指针

任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。

*Go语言中的指针不能进行偏移和运算*,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&(取地址)和*(根据地址取值)。

指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int*int64*string等。

取变量指针的语法如下:

1
ptr := &v    // v的类型为T

其中:

  • v:代表被取地址的变量,类型为T
  • ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

举个例子:

1
2
3
4
5
6
7
func main() {
	a := 10
	b := &a
	fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
	fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
	fmt.Println(&b)                    // 0xc00000e018
}

我们来看一下b := &a的图示:取变量地址图示

指针取值

在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。

1
2
3
4
5
6
7
8
9
func main() {
	//指针取值
	a := 10
	b := &a // 取变量a的地址,将指针保存到b中
	fmt.Printf("type of b:%T\n", b)
	c := *b // 指针取值(根据指针去内存取值)
	fmt.Printf("type of c:%T\n", c)
	fmt.Printf("value of c:%v\n", c)
}

输出如下:

1
2
3
type of b:*int
type of c:int
value of c:10

总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值

指针传值示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func modify1(x int) {
	x = 100
}

func modify2(x *int) {
	*x = 100
}

func main() {
	a := 10
	modify1(a)
	fmt.Println(a) // 10
	modify2(&a)
	fmt.Printl
    n(a) // 100
}

结构体和接口

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。

Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

类型别名和自定义类型

自定义类型

Go语言中可以使用type关键字来定义自定义如stringintbool等的数据类型。

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过struct定义。例如:

1
2
//将MyInt定义为int类型
type MyInt int

通过type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

类型别名

类型别名规定:本质上是同一个类型。就像一个孩子小时候有小名,这和他的名字都指向同一个人。

1
type 类型的别名 = 类型名

类型定义和类型别名的区别

类型别名 和原类型是同一种类型。自定义类型是一种全新的类型。

类型别名的类型只会在代码中存在,编译完成时并不会存在。

类型别名 和 自定义类型 的意义

自定义类型 : 举个例子,我们想给 int 类型定义一个方法,但是又不想改变int本身的性质。可以基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

类型别名: 想象一下你有一个非常长的类型名字,比如map[int]string,如果在代码中反复使用这个类型,那将会变得很啰嗦。

但是如果你使用类型别名来代替它,比如data,那么你只需使用Data这个简短的名字就可以代替长长的类型名字了

结构体

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct

也就是我们可以通过struct来定义自己的类型了。

结构体的定义

使用typestruct关键字来定义结构体,具体代码格式如下:

1
2
3
4
5
type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    
}

其中:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:表示结构体字段的具体类型。

举个例子,我们定义一个Person(人)结构体,代码如下:

1
2
3
4
5
type person struct {
	name string
	city string
	age  int8
}

这样我们就拥有了一个person的自定义类型。

我们通过.来访问结构体的字段(成员变量),例如p1.namep1.age等。

1
2
3
var p1 person
p1.name = "zero"
p1.age = "18"

结构体初始化

没有初始化的结构体,其成员变量都是对应其类型的零值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type person struct {  // 创建一个结构体类型,像int这种类型一样
	name string
	city string
	age  int8
}

func main() {
	var p4 person  //将结构体类型实例化,但是没有初始化
	fmt.Printf("p4=%#v\n", p4)  //p4=main.person{name:"", city:"", age:0}
}

使用键值对初始化

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

1
2
3
4
5
6
p5 := person{
	name: "小王子",
	city: "北京",
	age:  18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"小王子", city:"北京", age:18}

PS : golang 结构体的初始化有很多方式,课件有参考链接,建议大家下去看看(会用就好)

嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针,就像下面的示例代码那样。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Address 地址结构体
type Address struct {
	Province string
	City     string
}

//User 用户结构体
type User struct {
	Name    string
	Gender  string
	Address Address
}

func main() {
	user1 := User{
		Name:   "小王子",
		Gender: "男",
		Address: Address{
			Province: "山东",
			City:     "威海",
		},
	}
	fmt.Printf("user1=%#v\n", user1)//user1=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
}

方法和接收者

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)

只有特定的接收者变量才可以调用对应的方法。

方法的定义格式如下:

1
2
3
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}

其中,

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写。例如,Person类型的接收者变量应该命名为 p
  • 接收者类型:接收者类型和参数类似,可以是指针类型非指针类型
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 结构体
type Person struct {
	name string
	age  int
}

//NewPerson 构造函数
func NewPerson(name string, age int) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

// 为 person这个类型创建一个学习的方法
func (p Person) Study(){
    fmt.Print("我要卷四各位,或者被各位卷四")
}

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄。

1
2
3
4
5
// SetAge 设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int) {
	p.age = newAge
}

调用该方法:

1
2
3
4
5
6
func main() {
	p1 := NewPerson("小王子", 25)
	fmt.Println(p1.age) // 25
	p1.SetAge(30)
	fmt.Println(p1.age) // 30
}

值类型的接收者

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// SetAge2 设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
	p.age = newAge
}

func main() {
	p1 := NewPerson("小王子", 25)
	p1.Dream()
	fmt.Println(p1.age) // 25
	p1.SetAge2(30) // (*p1).SetAge2(30)
	fmt.Println(p1.age) // 25
}

什么时候应该使用指针类型接收者

  1. 需要修改接收者中的值

  2. 接收者是拷贝代价比较大的大对象

  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

接口

接口是一种类型

接口是一种由程序员来定义的类型,一个接口类型就是一组方法的集合,它规定了需要实现的所有方法。

相较于使用结构体类型,当我们使用接口类型说明相比于它是什么更关心它能做什么,而不是它是什么。

接口的定义

每个接口类型由任意个方法签名组成,接口的定义格式如下:

1
2
3
4
5
type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2
    
}

其中:

  • 接口类型名:Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有关闭操作的接口叫closer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子,定义一个包含Write方法的Writer接口。

1
2
3
type Writer interface{
    Write([]byte) error
}

当你看到一个Writer接口类型的值时,你不知道它是什么,唯一知道的就是可以通过调用它的Write方法来做一些事情。

实现接口的条件

接口就是规定了一个需要实现的方法列表,在 Go 语言中一个类型只要实现了接口中规定的所有方法,那么我们就称它实现了这个接口。

我们定义的Singer接口类型,它包含一个Sing方法。

1
2
3
4
// Singer 接口 可以称之为 “会唱歌的东西”
type Singer interface {
	Sing()
}

我们有一个Bird结构体类型如下。

1
type Bird struct {}

因为Singer接口只包含一个Sing方法,所以只需要给Bird结构体添加一个Sing方法就可以满足Singer接口的要求。

1
2
3
4
// Sing Bird类型的Sing方法
func (b Bird) Sing() {
	fmt.Println("汪汪汪")
}

这样就称为Bird实现了Singer接口。bird 就属于singer 类型了。

接口的意义

1️⃣ 实现多态性(Polymorphism):通过接口,可以实现多个不同类型的对象以相同的方式进行操作,这增强了代码的灵活性和可复用性。

2️⃣ 解耦合(Decoupling):接口使得模块之间的依赖关系更松散,模块只需要关注接口定义的方法,而不需要知道具体的实现细节。这有助于降低代码的耦合度,增加代码的可维护性和可测试性。

3️⃣ 扩展性(Extensibility):通过接口定义通用的行为,可以方便地对系统进行扩展和修改,而不需要改动已有的代码。当需要添加新的功能时,只需要实现接口定义的方法即可。

4️⃣ 接口断言(Interface Assertion):使用接口断言可以在运行时检查一个对象是否实现了某个接口,并根据情况进行处理。这样可以进行更灵活的类型转换和错误处理。

小练习

你要设计一个电子商务平台,该平台有多种类型的商品,例如电子产品、家居用品和服装等。你需要设计以下结构体和功能:

  1. 实现商品结构体:包含商品的名称、价格和库存数量等信息。

  2. 实现接口:定义商品的库存管理功能,包括检查库存数量、更新库存数量和打印库存信息等。

  3. 实现电子产品结构体:继承自商品结构体,同时具有电子产品特有的属性,例如品牌和型号。

  4. 实现接口:定义电子产品结构体的库存管理功能(同上),以及打印品牌型号信息的功能。

    完成后提交到邮箱 2926310865@qq.com

    有什么问题也可以发邮箱问哦

参考链接

Go语言基础之包 | 李文周的博客 (liwenzhou.com)

Go语言基础之指针 | 李文周的博客 (liwenzhou.com)

Go语言基础之结构体 | 李文周的博客 (liwenzhou.com)

Go语言基础之接口 | 李文周的博客 (liwenzhou.com)