内容简介:在Golang中,接口(Interface)包含两层意思,一是一系列方法的集合,而是代表一种类型,比如接口类型,整数类型。以我们比较熟悉的数据库为例,一个数据库一般会有打开和关闭操作,所以我们可以定义这样一个接口但这样定义没有用,我们还要实现这个接口,毕竟当我们存储数据的时候,需要一个明确的数据库,比如MySQL,或者MongoDB。
在Golang中,接口(Interface)包含两层意思,一是一系列方法的集合,而是代表一种类型,比如接口类型,整数类型。
接口是一系列方法的集合
以我们比较熟悉的数据库为例,一个数据库一般会有打开和关闭操作,所以我们可以定义这样一个接口
// 数据库接口,包含 openDB 和 closeDB两个方法 type Database interface { openDB() closeDB() } 复制代码
但这样定义没有用,我们还要实现这个接口,毕竟当我们存储数据的时候,需要一个明确的数据库,比如MySQL,或者MongoDB。
// Golang中的接口是自动实现的,当你的结构体包含接口中所有方法时,注意是所有,则Golang解释器会认为 MySQL 实现了 Database 这个接口 type MySQL struct { } func (mysql *MySQL) openDB() { fmt.Println("open mysql") } func (mysql *MySQL) closeDB() { fmt.Println("close mysql") } 复制代码
当你想再扩展一个数据库时,比如MongoDB,只需实现同样的方法即可,非常方便
type MongoDB struct { } func (mongo *MongoDB) openDB() { fmt.Println("open mongodb") } func (mongo *MongoDB) closeDB() { fmt.Println("open mongodb") } 复制代码
其实说了这么多,接口到底有什么用呢?它的作用就是解耦,让我们可以不用关心底层实现,还是就是方便扩展。
再举个使用的栗子,如果我们不用接口,且一开始使用的是MySQL数据库,我们的业务可能是这样子的:
// login.go mysql := &MySQL{} func login() { mysql.openDB() // 执行登录的逻辑 mysql.checkUser() ... } func getUInfo() { mysql.openDB() // 执行逻辑 mysql.checkUser() ... } 复制代码
从上面的例子可以看到,如果不用接口,我们的代码会充斥着很多 mysql
,如果有一天你需要把数据库换成 MongoDB
,你就会发现你得把这些接口都换成 MongoDB 的,非常麻烦。
也许从上面的例子上看,更换数据库只是批量更换 mysql
这个字符串而已,但也许实际业务远比这个复杂得多。
而当我们使用接口后,业务代码就会变成这样子:
var DBT Database func init(dbType string) { if dbType == "mysql" { DBT = &MySQL{} } else { DBT = &MongoDB{} } } func login() { DBT.openDB() // 执行登录的逻辑 DBT.checkUser() } func getUInfo() { DBT.openDB() // 执行逻辑 DBT.checkUser() ... } 复制代码
你会发现业务代码已经没有了mysql的身影,因为对业务代码来说,它确实不需要关心我使用什么数据库,只需要关心逻辑对不对就行了。
当你想切换其他数据库时,只需要更换dbType参数,并实现Database这个接口的所有方法即可,是不是轻松了很多?
杂谈: 其实你也可以将接口当成一个对象,一个对象会有很多方法属性,当你实现的方法越多,你就跟这个接口(对象)越像。
接口也是一种类型
interface{} 类型是没有方法的接口。
由于这个接口没有方法,等同于其他类型(整数、字符串等)都实现了这个接口,所以我们可以看到当其作为参数时,可以传入任何类型的变量。
func interfaceType(data interface{}) { fmt.Println("data", data) } // 可传入字符串和整数 func interfaceTypeTest() { interfaceType(111) interfaceType("111") } 复制代码
但是,凡事都有例外,比如大多数人犯的一个错误就是定义了一个 []interface{} 参数,然后以为 []int
和 []string
都可以传入。
func sliceInterface(dataList []interface{}) { for _, data := range dataList { fmt.Println(data) } } func mapInterface(dataMap map[string]interface{}) { fmt.Println(dataMap) } // 错误的做法 func sliMapErrorInterfaceTest() { nums := []int{1, 2, 3, 4} id2name := map[int]string{1: "aa", 2: "bb", 3: "cc"} // 报错:Cannot use 'nums'(type []int) as type []interface{} sliceInterface(nums) // 报错:Cannot use 'id2name'(type map[int]string) as type map[string]interface{} mapInterface(id2name) } 复制代码
正确的做法是需要自己手动做一层转换,像下面的代码:
// 正确的做法 func sliMapCorrectInterfaceTest() { nums := []int{1, 2, 3, 4} id2name := map[int]string{1: "aa", 2: "bb", 3: "cc"} // 需手动转换 numsI := []interface{}{} id2nameI := make(map[int]interface{}) for _, num := range nums { numsI = append(numsI, num) } for k, v := range id2name { id2nameI[k] = v } sliceInterface(numsI) mapInterface(id2nameI) } 复制代码
这是因为 []interface{}
实际是一个切片类型,只不过它的内容刚好是 interface{}
类型。这样来讲肯定还是有人不明白,所以官方文档也从内存的角度来阐述它的不同。
[]interface{}
中的 interface{}
在内存中占了两个字符,第一个字符表示它包含的数据的类型,第二个字符表示所包含的数据或者指向它的指针,这也就意味着,对于
aa := []int{1, 2, 3} 可能只占 1 * 3 个字符,但是对于 aa := []interface{}{1, 2, 3} 则会占 2 * 3 = 6个字符。
所以当我们将上述的 nums 变量传递至sliceInterface时,由于类型本身就不匹配,且 Go 又没有对应的自动转换机制,所以就报错了。
参考: 官方文档
函数
1.形参和实参
之前学 Python 时,比较少接触这两个概念,所以做下备忘
// 形参就是方法定义的参数,如下面的变量a;实参就是实际传进的参数,比如下面的变量b func test(a string){ fmt.Println(a) } b := "aa" test(b) 复制代码
2.Go的参数和返回值
2.1 Go的参数类型在参数名后面,返回值在参数后面
// x,y是传递的参数,最终返回int类型 func add(x int, y int) int { return x + y } 复制代码
2.2 类型共享
// x, y类型一致,只需要声明一个类型即可 func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return x, y } 复制代码
2.3 一个 return
关键字返回所有值,这种方式Go文档称为 Named return values
,不建议在比较复杂的函数内使用
func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return // 如下,这里的return关键字等同于return x, y } 复制代码
3.可变参数
Golang的可变参数使用 ...
符号实现
3.1 同一类型的不定参数
// 不定参数,numbers等同于一个切片 func indefiniteParams(numbers ...int) { fmt.Println(reflect.TypeOf(numbers)) // []int for _, num := range numbers { fmt.Println(num) } } func indefiniteParamsTest() { indefiniteParams(1, 2, 3) } 复制代码
3.2 不同类型的不定参数
func diffTypeParams(args ...interface{}) { for _, arg := range args { //迭代不定参数 switch arg.(type) { case int: fmt.Println(arg, "is int") case string: fmt.Println(arg, "is string") case float64: fmt.Println(arg, "is float64") case bool: fmt.Println(arg, "is bool") default: fmt.Println("未知类型") } } } func diffTypeParamsTest() { diffTypeParams(11, 11.1, "22", false) } 复制代码
4.结构体方法
结构体方法,也可以简单理解为类方法
详细请戳: Go指南-结构体与指针
5.闭包的实现
func getSequence() func() int { i := 0 fmt.Println("i", i) return func() int { i += 1 return i } } func getSequenceTest() { // 初始化, 返回函数,此时 nextNumber 等价于 func() int { i += 1; return i } nextNumber := getSequence() // 由于nextNumber本质是一个函数,nextNumber()即执行该函数,只不过i的值会保留,所以i的值会一直累加 fmt.Println(nextNumber()) // 1 fmt.Println(nextNumber()) // 2 fmt.Println(nextNumber()) // 3 } 复制代码
欢迎关注我们的微信公众号,每天学习Go知识
以上所述就是小编给大家介绍的《Go指南-谈谈Go的接口与函数》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。