Golang笔记02:函数、方法、泛型、接口学习笔记
一、进阶学习
1.1、函数
go
中的函数使用func
关键字进行定义,go
程序的入口函数叫做:main
,并且必须是属于main包
里面。
1.1.1、定义函数
(1)普通函数
go
中定义函数,需要使用func
关键字,同时要指定函数名称,函数参数,函数返回值,函数体,这就是函数的五要素。语法格式:
func 函数名称(参数1,参数2,...)(返回值类型1,返回值类型2,...) {// 函数体
}// 当只有一个返回值类型的时候,可以省略括号(),直接写类型即可
go
语言中,函数允许返回多个返回值
,这个语法和其他的一些编程语言是有些区别的,比如:Java
语言中,只允许一个函数返回一个返回值。go
语言中,函数的特点:
- 允许返回多个返回值。
- 不允许函数重载。
// Method01 定义函数
func Method01() int {return 1 + 2
}// Method02 定义函数
func Method02(a int, b int) (int,int) {return a + b, a - b;
}
另外,如果函数参数的类型都是一致的,那么可以简写成下面这种方式:
// Method02 定义函数,参数类型相同,则可以简写
func Method02(a, b int) (int,int) {return a + b, a - b;
}
(2)函数字面量
函数字面量,是指将函数作为一个变量先定义出来,接着在需要使用的时候,通过func
重新定义一个函数,然后函数字面量指向func
函数。如下所示:
package mainimport "fmt"// 先定义一个函数字面量,函数体不定义
var method func(int, int) (int, int)// Method02 定义函数
func Method02(a int, b int) (int, int) {return a + b, a - b
}func main() {// 这里给函数字面量,指向具体的函数实现method := func(a int, b int) (int, int) {return a + b, a - b}// 调用函数add, sub := method(1, 2)fmt.Println(add, sub)
}
1.1.2、函数参数和返回值
(1)函数参数
go
语言中的函数参数,如果是相同数据类型的,则可以统一定义类型,不需要每个参数都写出类型名称。格式:
func 函数名称(参数1,参数2,参数3 数据类型)(返回值1,返回值2...) {// 函数体
}
另外,go
中函数参数的传递是值传递
,也就是说,函数参数传递的时候,会拷贝实参的值。在函数体内修改函数参数的值,不会影响实参的值。
如果要定义可变参数,那么可以使用【...
】符号,并且可变参数只能够在最后一个参数位置出现。
// 可变参数
func Method03(args ...int) {}
(2)函数返回值
go
语言中,函数的返回值允许定义多个,当只有一个返回值,则可以不写括号,超过一个返回值的时候,则必须使用括号将所有返回值包裹起来。
// 只有一个返回值,可以省略返回值的括号
func demo() int {}
// 多返回值
func demo() (int,string){}
另外,go
中函数返回值也可以定义名称,定义返回值名称之后,那么这个参数就可以在函数体中直接使用,并且通过return
返回结果。
// Method02 定义函数
func Method02(a, b int) (add int,sub int) {// 可以使用函数返回值名称add = a + bsub = a - b// 定义了返回值名称,则return的时候,可以不写// 等价于 return add, subreturn//return add, sub
}
1.1.3、匿名函数
匿名函数,顾名思义,就是没有函数名称的函数。匿名函数一般在函数内部使用,语法格式:
package mainimport "fmt"func main() {// 定义匿名函数,并且调用函数ans := func(a, b int) int {ans := a + breturn ans}(1, 2)fmt.Println(ans)
}
匿名函数也可以作为一个函数的参数,例如:
package mainimport "fmt"// 定义函数,并且函数接受一个函数作为参数
func demo(a, b int, call func(int, int) int) int {return call(a, b)
}func main() {// 调用函数ans := demo(1, 2, func(a, b int) int {return a * b})fmt.Println(ans)// 调用函数ans2 := demo(1, 2, func(a, b int) int {return a + b})fmt.Println(ans2)
}
1.1.4、闭包
go
语言中,函数有一个闭包的概念,闭包在一些编程语言中,又叫做:Lamda
表达式。闭包,可以在内部函数中访问到外部函数的变量,即使外部函数已经执行完毕,内部函数仍然可以访问到外部的变量,这个过程就叫做:闭包。
- 【
闭包=匿名函数+外部环境变量引用
】
下面给一个闭包的案例:
package mainimport "fmt"func main() {// 创建一个匿名函数,并且返回值是一个函数类型sum := func() func() int {a, b := 1, 1// 返回一个匿名函数,相当于回调函数,并且还修改外部函数 a、b 两个变量的值return func() int {// 引用外部函数的变量a, b = b, a+breturn a}}// 调用函数,并且返回值是个回调函数getSum := sum()for i := 0; i < 10; i++ {// 调用回调函数,由于闭包的特性,回调函数中仍然可以使用 sum() 函数中的变量 a、bfmt.Println(getSum())}
}
闭包结构中,外部函数执行结束之后,内部函数就相当于一个回调函数一样,仍然可以被继续调用,并且还可以使用外部函数中的变量。另外,多次调用外部函数获取到的回调函数,是互不影响的。
package mainimport "fmt"func demo() func() int {count := 0return func() int {count++return count}
}func main() {// 第一次调用外部函数f1 := demo()fmt.Println("调用f1()函数")fmt.Println(f1())fmt.Println(f1())fmt.Println(f1())// 第二次调用外部函数f2 := demo()fmt.Println("调用f2()函数")fmt.Println(f2())fmt.Println(f2())fmt.Println(f2())fmt.Println("再次调用 f1() 函数")// 再次调用 f1() 函数fmt.Println(f1())
}
上面案例中,就是演示的多次调用外部函数,获取到的回调函数之间是互不影响的。运行结果如下所示:
调用f1()函数
1
2
3
调用f2()函数
1
2
3
再次调用 f1() 函数
4
从上面的执行结果可以看到,f1()
和f2()
之间的闭包结构是互不影响的。
1.1.5、延迟调用
go
语言中,提供了一个defer
关键字,可以用于声明函数的延迟调用功能。defer
声明的延迟函数,会在外部函数执行完成之前,调用延迟函数,一般用于释放文件资源,关闭连接等操作。
注意:当一个函数中,存在多个
defer
定义的延迟函数,那么go
会根据后进先出的规则,依次调用延迟函数。
怎么理解执行完成之前,才会调用defer
延迟函数呢???
- 当声明了
defer
函数的代码块内,执行到最后一行代码语句,后面已经没有可执行的语句的时候,但是函数还没有结束执行,只有遇到函数的最后一个右花括号【}】,才算执行结束。 - 那么
defer
函数,就是在遇到右花括号【}】之前,会被调用的。
package mainimport "fmt"func deferDemo() {fmt.Println("3、执行延迟函数...")
}func main() {fmt.Println("1、执行main函数代码...")// 采用延迟函数的方式,调用方法,将在 main 函数执行完成之前,调用 deferDemo() 函数defer deferDemo()fmt.Println("2、执行main函数最后一句代码...")
}// 控制台输入结果
1、执行main函数代码...
2、执行main函数最后一句代码...
3、执行延迟函数...
从上面案例代码中,可以看到,虽然defer
函数声明在中间位置,但是从控制台输出的结果来看,defer
函数的内容确是最后打印出来的,这也就说明defer
函数是最后执行的。
注意了,当存在多个defer延迟函数的时候,Go语言会按照后进先出的顺序,依次执行defer延迟函数。案例代码:
package mainimport "fmt"func demo01() {fmt.Println("调用demo01()函数...")
}
func demo02() {fmt.Println("调用demo02()函数...")
}
func demo03() {fmt.Println("调用demo03()函数...")
}func main() {fmt.Println("开始执行main函数...")// 定义延迟函数defer demo02()defer demo01()defer demo03()fmt.Println("main函数执行完成...")
}// 执行结果
开始执行main函数...
main函数执行完成...
调用demo03()函数...
调用demo01()函数...
调用demo02()函数...
1.2、方法
go语言中,既有函数,又有方法,在其他的语言中,一般情况下,函数和方法都是同一个概念,但是在go语言里面,两者是有点不同的。
go中的函数和方法,在定义形式上大体相同,只不过方法的定义,需要显示的声明方法的接收者,只有接收者才可以调用方法。方法定义格式:
// 自定义方法接收者的类型
type 接收者类型名称 接收者实际数据类型func (方法接收者名称 接收者类型名称) 方法名称(方法参数) 返回值类 {// 方法体
}
什么是方法接收者呢???要如何理解方法接收者这个概念???
- 方法的接收者,可以理解成是方法的调用者,它是规定哪一种数据类型的对象,可以调用这个方法。
go
语言中的方法接收者,就类似于java
语言中的this
关键字,例如:this.demo()
,这个this
就是方法的接收者,也可以理解成调用者。
方法的调用一般需要和自定义类型结合使用。
调用方法的语法格式:
变量名称 := 值
变量名称.方法名称()
方法的调用和函数的调用有点区别,函数是直接在代码中调用即可,不需要指定是由谁触发的调用。而方法,则需要指定具体的调用对象,是由哪个变量对象触发的方法调用。
package mainimport "fmt"// MyInt 先自定义一个类型
type MyInt intfunc (myInt MyInt) setValue(value int) {// 修改参数值myInt = MyInt(value)fmt.Println("方法中,修改的值=", myInt)
}func main() {var myInt MyInt = 1fmt.Println("修改之前的值=", myInt)// 调用方法myInt.setValue(2)fmt.Println("调用方法,修改之后的值=", myInt)
}// 执行结果
修改之前的值= 1
方法中,修改的值= 2
调用方法,修改之后的值= 1
从上面案例代码中,可以看到,我们虽然调用了setValue()
方法去修改myInt
的变量,但是方法执行完成之后,myInt
的值仍然没有变化,这是为什么呢???
这就涉及到一个知识点了,方法的接收者分为两种情况:值接收者
和指针接收者
。
1.2.1、值接收者
go
中方法的值接收者,是指方法接收者是通过值传递到方法里面的,传递的是形参,修改形参是不会影响到实际参数的。这和Java
中的值传递的概念相同。
要想在方法里面,修改接收者的数据,那就需要通过指针接收者来实现。
1.2.2、指针接收者
方法指针接收者,这和Java
中的引用传递的概念相同,在一个方法中,对引用传递
的参数进行修改,实际上是对这个引用地址指向的数据进行了修改,会影响实际的参数。
go
中的指针接收者作用就和Java
引用传递作用相同,通过指针接收者修改数据,会将实际参数的值也一起更新了。
package mainimport "fmt"// MyInt 先自定义一个类型
type MyInt int// 方法接收者采用指针类型
func (myInt *MyInt) setValue(value int) {// 修改参数值*myInt = MyInt(value)fmt.Println("方法中,修改的值=", *myInt)
}func main() {var myInt MyInt = 1fmt.Println("修改之前的值=", myInt)// 调用方法,虽然 myInt 这里是值类型,但是 Go 会将其编译之后,就相当于是 (&myInt).setValue(2)myInt.setValue(2)fmt.Println("调用方法,修改之后的值=", myInt)
}// 执行结果
修改之前的值= 1
方法中,修改的值= 2
调用方法,修改之后的值= 2
1.3、接口介绍
Go
中的接口分为两大类:基本接口
和通用接口
。
- 基本接口:接口内部是一组方法的集合。
- 通用接口:接口内部是一组类型的集合。
接口,是一组规范的集合,也就是说,接口只会规定实现功能的规范,但是具体的功能逻辑是怎么实现的,接口不负责,具体的功能实现交给具体的实现类。
在Go
中没有类与继承的概念,而是通过结构体来实现类的功能,那么在接口实现上,也是通过结构体来实现的。你可以怎么理解,创建一个struct
结构体,就相当于是Java
中创建了一个Class
类。
1.3.1、基本接口
Go
中定义接口需要使用interface
关键字,这个关键字用于标识是接口类型。
(1)定义接口
基本接口的语法格式,如下所示:
// 基本接口的定义
type 接口名称 interface {// 定义方法方法名称(参数类型) 返回值类型方法名称(参数类型) 返回值类型方法名称(参数类型) 返回值类型
}
在定义基本接口的时候,接口内部只能够是一组方法的集合,不能存在其他的类型集合。针对接口中的方法,方法参数可以不用写参数名称,只需要指定类型即可,因为接口不具备实现逻辑,也就不会使用方法参数,所以写不写参数名称都没关系,但是类型是必须要规定的。
package mainimport "fmt"// BaseInterface 定义基本接口
type BaseInterface interface {// Say 定义两个方法Say(string) stringWalk()
}func main() {// 初始化接口var baseInterface BaseInterfacefmt.Println(baseInterface)
}
上面代码,就是定义了一个基本接口,并且在main
函数中,初始化了接口,但是这个接口还没有具体实现,只是初始化了。
(2)接口实现
接口定义好了之后,那么要如何实现这个接口中的方法呢???Go
语言中没有提供类似Java
中的implements
关键字,那么要怎么样才能实现接口呢???
在Go
语言中,接口的实现方式很简单,只需要一种类型(结构体或者自定义类型
)将接口中的所有方法都实现了,那么我们就说,这个类型实现了某某接口。
package mainimport "fmt"// BaseInterface 定义基本接口
type BaseInterface interface {// Say 定义两个方法Say(string) stringWalk()
}// CustomType 定义类型,实现接口
type CustomType struct{}// Say CustomType 实现 BaseInterface 接口的方法
func (customType CustomType) Say(s string) string {fmt.Println("CustomType实现接口:saying..." + s)return s
}// Walk CustomType 实现 BaseInterface 接口的方法
func (customType CustomType) Walk() {fmt.Println("CustomType实现接口:walking...")
}func main() {
}
上面案例代码中,自定义了CustomType
结构体类型,然后这个结构体定义了两个方法,方法和接口中定义的方法名称一致,那么这种情况下,CustomType
类型就是实现了BaseInterface
接口了。
Go
语言中接口的实现都是隐式的,不像Java
中的接口实现那样,还需要使用implements
关键字才能够实现接口。
(3)使用接口
实现接口之后,就需要在相应的地方,使用接口实现来完成业务功能啦。使用接口很简单,其实就是通过接口的实现类,调用对应的接口方法即可。
package mainimport "fmt"// BaseInterface 定义基本接口
type BaseInterface interface {// Say 定义两个方法Say(string) stringWalk()
}// CustomType 定义类型,实现接口
type CustomType struct{}// Say CustomType 实现 BaseInterface 接口的方法
func (customType CustomType) Say(s string) string {fmt.Println("CustomType实现接口:saying..." + s)return s
}// Walk CustomType 实现 BaseInterface 接口的方法
func (customType CustomType) Walk() {fmt.Println("CustomType实现接口:walking...")
}func main() {// 定义接口实现类customType := CustomType{}// 调用接口方法say := customType.Say("Hello World")fmt.Println("返回值:" + say)customType.Walk()
}
从上面案例代码中,可以发现一个问题,在使用接口的时候,我们的代码里面没有直接出现BaseInterface
这个接口,而是定义了一个CustomType
结构体的变量,然后通过结构体变量去调用了结构体实现的Say
和Walk
两个方法。
这是因为Go
语言中,某个类型(结构体或者自定义类型
)实现某个接口之后,对应的类型就已经满足接口中定义的方法规范。
(4)空接口
Go
中提供了一个空接口,空接口就是接口中没有定义任何的方法,里面是空的。空接口相当于Java
中的Object
对象,是所有类型的父类型,Go
中的空接口就是所有类型的父类型。
// 定义空接口变量
interfaceVar interface{}
空接口可以作为一个方法的参数,这个参数叫做:空接口变量
。空接口变量可以接收任意的数据类型,从而就可以实现同一个方法可以处理多种类型的参数。
package mainimport "fmt"func demo(interfaceVar interface{}) {switch v := interfaceVar.(type) {case int:fmt.Println("int 类型", v)case string:fmt.Println("string 类型", v)default:fmt.Println("都不满足的类型")}
}func main() {// 定义int类型myInt := 10demo(myInt)// 定义string类型myStr := "hello world"demo(myStr)
}
(5)类型切换和断言
Go
语言中,提供了一种特殊的语法,叫做:类型切换
和类型断言
。语法格式:
// 类型切换和类型断言
v := interfaceVar.(T)// interfaceVar 表示接口变量
// T 表示数据类型
// v 是一个变量,具体来说是一个动态类型变量,它的类型会根据 interfaceVar.(T) 检测出来的类型变化
// 当 v 的类型和 case 类型匹配时候,那么 v 变量就会被赋值对应类型的数据值
上面表达式用在switch
结构里面,能够动态的检查接口变量interfaceVar
的具体类型,并且将变量的值赋值给变量v
。
类型切换和类型断言的代码执行规则,我理解大概是这样的:
1、首先进行类型切换,通过 interfaceVar.(T),获取到具体的类型
2、将获取到具体类型赋值给 v 变量
3、v 变量的类型再和 case 中的类型进行匹配
4、如果 v 能够匹配上 case 的类型,则将对应类型的值赋值给 v 变量
案例代码:
package mainimport "fmt"func demo(interfaceVar interface{}) {switch v := interfaceVar.(type) {case int:fmt.Println("int 类型", v)case string:fmt.Println("string 类型", v)default:fmt.Println("都不满足的类型")}
}func main() {// 定义int类型myInt := 10demo(myInt)// 定义string类型myStr := "hello world"demo(myStr)// 定义 float 类型myFloat := 3.14demo(myFloat)
}
1.3.2、通用接口
go中的通用接口,需要和泛型结合使用,这样才可以定义一个通用的接口,让所有的数据类型都适用,这就是通用接口。后续介绍泛型时候,在一起介绍通用接口。
1.4、泛型
Go
语言在1.18
版本中引入泛型的概念。泛型定义的语法格式如下所示:
// 泛型定义
[泛型参数 约束类型1 | 约束类型2...]// 举个例子:
func sum[T int | float32](a,b T) T {// 方法体
}
类型约束
是指:当前泛型可以接收哪些数据类型,如果不是这里面的类型,那么就无法使用对应的方法、函数之类的。
上面就是定义泛型时候的语法格式,那要如何使用呢???
在使用泛型的时候,可以有两种方式:
- 第一种方式:使用时候指定具体的数据类型。
- 第二种方式:不指定类型,让编译期自动推断类型。
package mainimport "fmt"// 定义泛型方法
func sum[T int | float32](a, b T) T {return a + b
}func main() {// 计算 int 类型ans := sum(1, 2)fmt.Println(ans)// 主动指定类型ans2 := sum[float32](3.14, 2.86)fmt.Println(ans2)
}
泛型结构的使用注意事项:
- 泛型不能用在基本类型上。
- 泛型不能进行类型断言。
- 匿名结构中,不允许使用泛型。
- 匿名函数不支持自定义泛型。
- 方法不能使用泛型。
为什么方法上面不能使用泛型呢???
我是这么理解的,因为
Go
中不允许方法重载,所以如果方法上面使用了泛型,那不就是相当于Go
中会存在两个相同名称的方法了吗?这就和Go
中不允许方法重载的定义相违背了,所以也就不允许方法使用泛型了。
1.5、类型
Go
语言中的类型,是一种静态强类型
,什么是静态呢???
静态强类型
静态是指:Go
中的数据类型一旦定义出来之后,在编译期期间就已经确定了,后续运行程序的时候,就不能够改变类型了。
强类型是指:当我们程序员在写代码的时候,如果改变了数据类型,那么编译期就会马上提示程序员,语法错了,不能修改类型。
var a int = 1
// 编译不通过,因为前后类型不一致
a = "2"
类型后置
Go
语言中,所有的数据类型都是写在名称后面的,这是因为写在变量名称后面,能够让程序的可读性更强,类型太多的时候不至于看着混乱。
var 变量名称 数据类型
类型声明
Go
语言中,使用type
关键字声明类型,自定义类型也是使用type
关键字定义的。
// 声明类型
type 类型名称 数据类型
类型转换
Go
语言中,没有类型Java
语言中的隐式类型转换的功能,Go
语言只有显式类型转换,也就是说,必须让程序员在代码中主动的进行类型转换。
package mainimport "fmt"func main() {var f float64 = 3.14// 强制类型转换var f2 int = int(f)fmt.Println(f2)
}
以上就是Go
语言中函数、方法、泛型、接口相关的学习笔记内容。