包与接口详解
- 1、Golang包详解
- 1.1、Golang中包的定义和介绍
- 1.2、Golang包管理工具go mod
- 1.3、Golang中自定义包
- 1.4、Golang中使用第三包
- 1.5、init函数
- 2、接口详解
- 2.1、接口的定义
- 2.2、空接口
- 2.3、类型断言
- 2.4、结构体值接收者和指针接收者实现接口的区别
- 2.5、一个结构体实现多个接口
- 2.6、接口嵌套
- 2.7、空接口和类型断言使用细节
1、Golang包详解
1.1、Golang中包的定义和介绍
包(package)是多个 Go 源码的集合,是一种高级的代码复用方案,Go语言为我们提供了很多内置包,如fmt、strconv、strings、sort、errors、time、encoding/json、os、io等。
Golang 中的包可以分为三种:1、系统内置包。2、自定义包。3、第三方包。
系统内置包: Golang语言给我们提供的内置包,引入后可以直接使用,如 fmt、 strconv、 strings、sort、errors、time、encoding/json、os、io等。
自定义包:开发者自己写的包。
第三方包:属于自定义包的一种,需要下载安装到本地后才可以使用,如前面给大家介绍的。
"github.com/shopspring/decimal"包解决float精度丢失问题。
1.2、Golang包管理工具go mod
在Golang1.11版本之前如果我们要自定义包的话必须把项目放在GOPATH 目录。Go1.11版本之后无需手动配置环境变量,使用 go mod 管理项目,也不需要非得把项目放到 GOPATH指定目录下,你可以在你磁盘的任何位置新建一个项目,Go1.13 以后可以彻底不要GOPATH了。
首先在项目路径下使用go mod init 文件名,来初始化项目,生成一个go.mod文件。
注意,这个文件名必须是当前所在目录下的文件名,比如当前我在demo60目录下,因此文件名就是demo60。
另外,使用命令go mod可以查看其他选项,在这些选项中我们最常用的就是init和tidy。
其中go mod tidy表示增加丢失的module,去掉未用的module,当我们执行这个指令后,就会自动将我们需要包下载下来,将不需要的包去除。
1.3、Golang中自定义包
包(package)是多个Go源码的集合,一个包可以简单理解为一个存放多个.go文件的文件夹。该文件夹下面的所有go文件都要在代码的第一行添加如下代码,声明该文件归属的包。
如图:我们在demo60目录下创建一个clac目录,并创建一个clac。
package main
import (
"demo60/clac"
"fmt"
)
func main() {
res1 := clac.Add(5, 10)
res2 := clac.Sub(5, 10)
fmt.Println(res1, res2)
}
接着我们在main.go中引入我们自定义的clac包,并调用里面的Add和Sub方法。
同时我们需要注意,在clac这个包中,可以定义变量和函数,但是必须是大写的,大写的表示公有属性,可以被其他包访问,小写表示私有属性,不能被其他包使用。
当然我们也可以引入多个包,例如我们在demo60目录下创建一个tools文件夹,里面再定义几个.go文件,但他们都属于同一个包tools。
package tools
import "fmt"
func PrintInfo() {
fmt.Println("hello world")
}
package tools
func Mul(x, y int) int {
return x * y
}
然后我们在main.go中调用这两个函数。
package main
import (
"demo60/clac"
"demo60/tools"
"fmt"
)
func main() {
res1 := clac.Add(5, 10)
res2 := clac.Sub(5, 10)
fmt.Println(res1, res2)
fmt.Println(clac.AA)
fmt.Println(tools.Mul(2, 3))
tools.PrintInfo()
}
上面就演示了我们自定义了两个包clac和tools,并通过import引入使用。
另外,我们也可以匿名导入一个包,不过在现在vscode的go语法已经非常智能了,所以基本上就用不到了。
例如下面这种情况,我们导入了一个fmt包,但是并没有使用,我们可以在前面加上_
package main
import _ "fmt"
func main() {
}
我们也可以给导入的包起别名,比如我们导入的一个包名字过长,我们就可以给他起个别名。
package main
import f "fmt"
func main() {
f.Println("hello world")
}
1.4、Golang中使用第三包
之前我们说过,Golang中的浮点数类型是有精度丢失的,所以我们可以使用第三方包decimal,下面演示如何使用第三方包decimal。
首先在项目目录下执行go mod init初始化项目,然后实现main.go文件。
package main
import (
"fmt"
"github.com/shopspring/decimal"
)
func main() {
num1 := 8.2
num2 := 3.8
d1 := decimal.NewFromFloat(num1).Add(decimal.NewFromFloat(num2))
fmt.Println(d1)
}
当我们这样写好代码保存后是无法运行的,因为我们并没有第三方包decimal。
所以这里就需要使用之前的go mod tidy。它会自动根据我们的文件查找是否有缺少的包,或者不需要使用的包。
1.5、init函数
在Go语言程序执行时导入包语句会自动触发包内部init()函数的调用。需要注意的是:init()函数没有参数也没有返回值。init()函数在程序运行时自动被调用执行,不能在代码中主动调用它。
Go语言包会从main包开始检查其导入的所有包,每个包中又可能导入了其他的包。Go 编译器由此构建出一个树状的包引用关系,再根据引用顺序决定编译顺序,依次编译这些包的代码。
在运行时,被最后导入的包会最先初始化并调用其init()函数,如下图示:
有点类似DFS的感觉。
2、接口详解
Golang中的接口是一种抽象数据类型,Golang中接口定义了对象的行为规范,只定义规范不实现。接口中定义的规范由具体的对象来实现。
通俗的讲接口就一个标准,它是对一个对象的行为和规范进行约定,约定实现接口的对象必须得按照接口的规范。
2.1、接口的定义
在Golang中接口(interface)是一种类型,函数method的集合,Golang中的接口不能包含任何变量。一种抽象的类型。接口(interface)是一组在Golang中接口中的所有方法都没有方法体,接口定义了一个对象的行为规范,只定义规范不实现。接口体现了程序设计的多态和高内聚低耦合的思想Golang中的接口也是一种数据类型,不需要显示实现。只需要一个变量含有接口类型中的所有方法,那么这个变量就实现了这个接口。
其中:
• 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。 接口名最好要能突出该接口的类型含义。
• 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
• 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。
实现一个Usber接口规范,接着让手机和相机实现这个接口。
package main
import "fmt"
type Usber interface {
start()
stop()
}
type Phone struct {
Name string
}
func (p Phone) start() {
fmt.Printf("%v启动\n", p.Name)
}
func (p Phone) stop() {
fmt.Printf("%v关闭\n", p.Name)
}
type Camera struct {
Name string
}
func (c Camera) start() {
fmt.Printf("%v启动\n", c.Name)
}
func (c Camera) stop() {
fmt.Printf("%v关闭\n", c.Name)
}
func main() {
p := Phone{
"小米手机",
}
c := Camera{
"哈苏相机",
}
var u1, u2 Usber // Golang中接口就是一个数据类型
u1 = p // 表示Phone实现接口Usber
u2 = c // 表示Camera实现接口Usber
u1.start()
u1.stop()
u2.start()
u2.stop()
}
下面我们可以再实现一个Computer结构体,然后实现它的一个方法work,该方法必须传入一个Usber接口。
package main
import "fmt"
type Usber interface {
start()
stop()
}
type Phone struct {
Name string
}
func (p Phone) start() {
fmt.Printf("%v启动\n", p.Name)
}
func (p Phone) stop() {
fmt.Printf("%v关闭\n", p.Name)
}
type Camera struct {
Name string
}
func (c Camera) start() {
fmt.Printf("%v启动\n", c.Name)
}
func (c Camera) stop() {
fmt.Printf("%v关闭\n", c.Name)
}
type Computer struct {
}
func (c Computer) work(u Usber) {
u.start()
u.stop()
}
func main() {
p := Phone{
"小米手机",
}
c := Camera{
"哈苏相机",
}
computer := Computer{}
computer.work(p)
computer.work(c)
}
2.2、空接口
Golang中的接口可以不定义任何方法,没有定义任何方法的接口就是空接口。空接口表示没有任何约束,因此任何类型变量都可以实现空接口。空接口在实际项目中用的是非常多的,用空接口可以表示任意数据类型。
1、定义空接口
空接口表示没有任何约束,任何类型都可以实现空接口。
package main
import "fmt"
type A interface{}
func main() {
var a A
var str = "你好golang"
a = str
fmt.Printf("值: %v, 类型: %T\n", a, a)
var num = 10
a = num
fmt.Printf("值: %v, 类型: %T\n", a, a)
var flag = true
a = flag
fmt.Printf("值: %v, 类型: %T\n", a, a)
}
2、空接口作为函数参数
package main
import "fmt"
func show(a interface{}) {
fmt.Printf("值: %v, 类型: %T\n", a, a)
}
func main() {
show(20)
show("你好golang")
show([]int{1, 2, 3, 4, 5})
}
3、map的值实现空接口,使用空接口实现可以保存任意值的字典。
package main
import "fmt"
func main() {
var m = make(map[string]interface{})
m["username"] = "张三"
m["age"] = 18
m["hobby"] = []string{"跑步", "爬山", "写代码"}
fmt.Printf("值: %v, 类型: %T\n", m, m)
}
4、切片实现空接口。
var slice = []interface{}{"张三", 18, true, 33.22}
fmt.Printf("值: %v, 类型: %T\n", slice, slice)
2.3、类型断言
一个接口的值 (简称接口值) 是由一个具体类型和具体类型的值两部分组成的。 这两部分分别称为接口的动态类型和动态值。
如果我们想要判断空接口中值的类型,那么这个时候就可以使用类型断言,其语法格式:
x.(T)
其中:
• x :表示类型为interface{}的变量
• T:表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为 T 类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为 alse则表示断言失败。
1、基本使用。
package main
import "fmt"
func main() {
var x interface{}
x = "hello golang"
v, ok := x.(string)
if ok {
fmt.Printf("类型为string, 值: %v\n", v)
} else {
fmt.Println("类型断言失败")
}
}
2、定义一个方法,可以传入任意类型,然后根据不同的类型实现不同的功能。
下面实现两种方式:if-else和switch-case。
package main
import "fmt"
func MyPrint1(x interface{}) {
if _, ok := x.(string); ok {
fmt.Println("string类型")
} else if _, ok := x.(int); ok {
fmt.Println("int类型")
} else if _, ok := x.(bool); ok {
fmt.Println("bool类型")
}
}
func MyPrint2(x interface{}) {
switch x.(type) {
case string:
fmt.Println("string类型")
case int:
fmt.Println("int类型")
case bool:
fmt.Println("bool类型")
default:
fmt.Println("传入错误...")
}
}
func main() {
MyPrint1("你好golang")
MyPrint2(12)
}
注意:x.(type)只能结合switch语句使用。
针对前面的实现的Computer,我们判断传入的类型然后实现对应的方法。有点类似于C++中的多肽。
package main
import "fmt"
type Usber interface {
start()
stop()
}
type Phone struct {
Name string
}
func (p Phone) start() {
fmt.Printf("%v启动\n", p.Name)
}
func (p Phone) stop() {
fmt.Printf("%v关闭\n", p.Name)
}
type Camera struct {
Name string
}
func (c Camera) start() {
fmt.Printf("%v启动\n", c.Name)
}
func (c Camera) stop() {
fmt.Printf("%v关闭\n", c.Name)
}
type Computer struct {
}
func (c Computer) work(u Usber) {
if _, ok := u.(Phone); ok {
u.start()
} else if _, ok := u.(Camera); ok {
u.stop()
}
}
func main() {
p := Phone{
"小米手机",
}
c := Camera{
"哈苏x2d",
}
com := Computer{}
com.work(p)
com.work(c)
}
在work函数中我们实现类型断言,如果是Phone类型就执行start函数,如果是Camera类型就执行stop函数。
2.4、结构体值接收者和指针接收者实现接口的区别
package main
import "fmt"
type Usber interface {
start()
stop()
}
type Phone struct {
Name string
}
func (p Phone) start() {
fmt.Printf("%v启动!\n", p.Name)
}
func (p Phone) stop() {
fmt.Printf("%v关闭!\n", p.Name)
}
func main() {
var u Usber
var p1 = Phone{
"小米手机",
}
u = p1
u.start()
u.stop()
var p2 = &Phone{
"苹果手机",
}
u = p2
u.start()
u.stop()
}
根据现象,如果结构体中的方法是值接受者,那么实例化后的结构体值类型和指针类型都可以赋值给接口变量。
将start和stop方法改为指针接受者类型*Phone,那么结构体值类型就无法复制给接口变量了,此时只有指针类型可以赋值给接口变量。
另外如果要修改结构体内的成员变量,必须采用指针接受者的方式,如下:
package main
import "fmt"
type Animaler interface {
setName(name string)
getName() string
}
type Dog struct {
Name string
}
func (d *Dog) setName(name string) {
d.Name = name
}
func (d Dog) getName() string {
return d.Name
}
func main() {
d := &Dog{
Name: "小柴",
}
var a Animaler
a = d
fmt.Println(a.getName())
a.setName("小奇")
fmt.Println(a.getName())
}
2.5、一个结构体实现多个接口
package main
import "fmt"
type Animaler1 interface {
getName() string
}
type Animaler2 interface {
setName(name string)
}
type Dog struct {
Name string
}
func (d Dog) getName() string {
return d.Name
}
func (d *Dog) setName(name string) {
d.Name = name
}
func main() {
d := &Dog{
Name: "小柴",
}
var a1 Animaler1
var a2 Animaler2
a1 = d
a2 = d
fmt.Println(a1.getName())
a2.setName("小奇")
fmt.Println(a1.getName())
}
2.6、接口嵌套
package main
import "fmt"
type Ainterface interface {
SetName(name string)
}
type Binterface interface {
GetName() string
}
type Animaler interface {
Ainterface
Binterface
}
type Dog struct {
Name string
}
func (d Dog) GetName() string {
return d.Name
}
func (d *Dog) SetName(name string) {
d.Name = name
}
func main() {
d := &Dog{
Name: "小柴",
}
var a Animaler
a = d
fmt.Println(a.GetName())
a.SetName("小奇")
fmt.Println(a.GetName())
}
2.7、空接口和类型断言使用细节
下面的代码有没有问题?
package main
import "fmt"
type Address struct {
Name string
Phone string
}
func main() {
var m = make(map[string]interface{})
m["username"] = "张三"
m["age"] = 20
m["hobby"] = []string{"跑步", "爬山", "写代码"}
fmt.Println(m["username"])
fmt.Println(m["age"])
fmt.Println(m["hobby"])
fmt.Println(m["hobby"][0])
}
可以看到报错了,不支持这么访问。
再来看另一种情况:
m["address"] = Address{
Name: "李四",
Phone: "13912344321",
}
fmt.Println(m["address"])
fmt.Println(m["address"].Name)
这样也是不支持的。
解决办法,实现类型断言接受后再访问:
package main
import "fmt"
type Address struct {
Name string
Phone string
}
func main() {
var m = make(map[string]interface{})
m["username"] = "张三"
m["age"] = 20
m["hobby"] = []string{"跑步", "爬山", "写代码"}
fmt.Println(m["username"])
fmt.Println(m["age"])
fmt.Println(m["hobby"])
hobby, _ := m["hobby"].([]string)
fmt.Println(hobby[0], hobby[1], hobby[2])
m["address"] = Address{
Name: "李四",
Phone: "13912344321",
}
fmt.Println(m["address"])
address, _ := m["address"].(Address)
fmt.Println(address.Name, address.Phone)
}