Golang——7、包与接口详解
包与接口详解
- 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 mainimport ("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 toolsimport "fmt"func PrintInfo() {fmt.Println("hello world")
}
package toolsfunc Mul(x, y int) int {return x * y
}
然后我们在main.go中调用这两个函数。
package mainimport ("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 mainimport _ "fmt"func main() {}
我们也可以给导入的包起别名,比如我们导入的一个包名字过长,我们就可以给他起个别名。
package mainimport f "fmt"func main() {f.Println("hello world")
}
1.4、Golang中使用第三包
之前我们说过,Golang中的浮点数类型是有精度丢失的,所以我们可以使用第三方包decimal,下面演示如何使用第三方包decimal。
首先在项目目录下执行go mod init初始化项目,然后实现main.go文件。
package mainimport ("fmt""github.com/shopspring/decimal"
)func main() {num1 := 8.2num2 := 3.8d1 := 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 mainimport "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实现接口Usberu2 = c // 表示Camera实现接口Usberu1.start()u1.stop()u2.start()u2.stop()
}
下面我们可以再实现一个Computer结构体,然后实现它的一个方法work,该方法必须传入一个Usber接口。
package mainimport "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 mainimport "fmt"type A interface{}func main() {var a Avar str = "你好golang"a = strfmt.Printf("值: %v, 类型: %T\n", a, a)var num = 10a = numfmt.Printf("值: %v, 类型: %T\n", a, a)var flag = truea = flagfmt.Printf("值: %v, 类型: %T\n", a, a)
}
2、空接口作为函数参数
package mainimport "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 mainimport "fmt"func main() {var m = make(map[string]interface{})m["username"] = "张三"m["age"] = 18m["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 mainimport "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 mainimport "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 mainimport "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 mainimport "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 Usbervar p1 = Phone{"小米手机",}u = p1u.start()u.stop()var p2 = &Phone{"苹果手机",}u = p2u.start()u.stop()
}
根据现象,如果结构体中的方法是值接受者,那么实例化后的结构体值类型和指针类型都可以赋值给接口变量。
将start和stop方法改为指针接受者类型*Phone,那么结构体值类型就无法复制给接口变量了,此时只有指针类型可以赋值给接口变量。
另外如果要修改结构体内的成员变量,必须采用指针接受者的方式,如下:
package mainimport "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 Animalera = dfmt.Println(a.getName())a.setName("小奇")fmt.Println(a.getName())
}
2.5、一个结构体实现多个接口
package mainimport "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 Animaler1var a2 Animaler2a1 = da2 = dfmt.Println(a1.getName())a2.setName("小奇")fmt.Println(a1.getName())
}
2.6、接口嵌套
package mainimport "fmt"type Ainterface interface {SetName(name string)
}type Binterface interface {GetName() string
}type Animaler interface {AinterfaceBinterface
}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 Animalera = dfmt.Println(a.GetName())a.SetName("小奇")fmt.Println(a.GetName())
}
2.7、空接口和类型断言使用细节
下面的代码有没有问题?
package mainimport "fmt"type Address struct {Name stringPhone string
}func main() {var m = make(map[string]interface{})m["username"] = "张三"m["age"] = 20m["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 mainimport "fmt"type Address struct {Name stringPhone string
}func main() {var m = make(map[string]interface{})m["username"] = "张三"m["age"] = 20m["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)
}