22

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

 3 years ago
source link: https://studygolang.com/articles/28370
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

在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知识

FveQFjN.jpg!web

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK