Go指南-谈谈Go的接口与函数

言淦 · · 476 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

接口

在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大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

476 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传