go语言基础-函数、方法、接口

Go语言中函数是第一类对象,支持具名和匿名函数。方法是与类型绑定的函数,通过将函数第一个参数移到函数名前实现。Go语言的接口是鸭子类型,隐式实现。此外,文章讨论了闭包的引用特性和潜在问题,以及类型转换和类型断言的概念。

函数

在go语言中,函数是第一类对象,甚至可以将函数保存在变量中,当然函数也有具名和匿名函数之分。Go语言中每个类型可以绑定一个函数,也称之为方法。

函数的声明

使用func关键字声明函数

  • 在go语言中,是大小写敏感的,大写字母开头的函数、变量都会被导出,对其他包可用

  • 小写字母开头的就不行

//具名函数
func Add(a int, b int) int {
    return a + b
}

//匿名函数
var add = func(a, b int) int {
    return a + b
}

函数返回值

go语言支持多个参数和多个返回值,参数和返回值都是以传值的方式和被调用这进行数据交换。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。

//多个参数和多个返回值
func sumAndMulty(a int, b int) (int, int) {
    return a + b, a * b
}

//可变数量的参数 more对应[]int 切片类型
func sum(a int, more ...int) int {
    for _, item := range more {
        a += item
    }
    return a
}

不仅函数的参数可以有名字,也可以给函数的返回值命名:可以通过名字来修改返回值,也可以通过defer语句在return语句之后修改返回值

func Inc() (v int) {
    defer func() { v++ }()
    return 10
}

defer语句延迟执行了一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量v,这种函数我们一般成为闭包。闭包对捕获的外部变量并不是以传值方式访问,而是以引用方式访问

但是闭包函数有可能带来一些隐含的问题:

    for i := 0; i < 2; i++ {
        defer func() { println(i) }()
    }
    // 输出 3 3 3

因为是传的引用,每个defer语句延迟执行的函数引用都是i,当循环结束i=3 因此 输出3

修复思路为每轮迭代中为闭包函数生成独有的变量

for i := 0; i < 3; i++ {
        i := i //每轮循环独有的局部变量i
        defer func() {println(i)}()
    }
    
    for i := 0; i < 3; i++ {
        //通过函数传入i
        //defer 语句会马上对调用参数求值
        defer func(i int) { println(i) }(i) //匿名函数传入参数
    }

方法

方法一般是面向对象编程的一个特性,c++就是在兼容c语言的基础之上支持了类等面向对象的特性。然后Java则号称纯粹的面向对象语言,因为Java中函数是不能独立存在的,每个函数必然属于某个类的。

//打开文件
func openFile(name string) (f *file, err error) {
    // ...
}

//关闭文件
func closeFile(f *file) error {
    //...
}

//读取文件
func readFile(f *file, offset int64, data []byte) int {
    //...
}

可以看到closeFile()、readFile()函数只针对file类型结构体的操作,这时候更加希望这类函数和操作对象的类型紧密绑定在一起。因此,go语言的做法是将这类函数的第一个参数移动到函数名开头

func (f *file)closeFile() error {
    //...
}

func (f *file)readFile(offset int64, data []byte) int {
    //...
}

将函数与操作对象的类型绑定在一起,从代码角度看虽然是一个小的改动,但是从编程哲学角度来看,GO语言实现面向对象,我们可以给任何自定义类型添加一个或多个方法。注意每种类型的对应的方法和类型的定义在同一个包中,值得注意的是,每个方法的名字必须是唯一的,方法和函数一样的不支持重载。

go语言不支持传统面向对象中的继承特性,而是通过结构体组合的方式实现了方法的继承

type cache struct {
    m map[string]string
    sync.Mutex
}

func (p *cache)lookUp(key string) string {
    p.Lock()
    defer p.Unlock()
    return p.m[key]
}

cache结构体通过嵌入一个匿名的sync.Mutex来继承他的方法Lock()和Unlock(),但是在p在调用这俩方法的时候,p并不是真正的接收者,而是会将他们展开为p.Mutex.Lock()和p.Mutex.Unlock(),这种展开会在编译期完成。所有继承来的方法的接收者参数任然是那个匿名成员的本身,而不是当前变量。因此这种继承方法并不能实现C++虚函数的多态特性。

传统的面向对象语言,子类运行的方法是动态绑定在对象的,因此基类实现的某些方法看到的this可能不是基类类型对应的对象,这个特性会导致基类方法运行的不确定性。

go语言通过嵌入匿名函数来“继承”基类的方法,this就是实现该方法的类型对象。编译的时候静态绑定的。

接口

go语言的接口类是对其他类型行为的抽象和概括,接口类型不会和特定的实现细节绑定在一起。很多面向对象的语言都有相似的接口概念,但go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。所谓的鸭子类型:只要走起来像鸭子、叫起来像鸭子,那么就可以把他当作鸭子。

GO语言中的,如果一个对象只要看起来像是某种接口的类型的实现,那么他就可以作为接口类型使用。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不用破坏这些类型原有的定义。当使用的类型来自不受我们控制的包时,这种设计尤其灵活有用。

我们可以通过控制自己的输出对象,将每个字符转换为大写字符后输出:

//Writer 接口
type io.Writer interface {
    Write(p []byte) (n int, err error)
}
//UpperWriter对象包含一个接口
type UpperWriter struct {
    io.Writer
}
// 实现该接口的方法
func (p *UpperWriter) Write(data []byte) (n int, err error) {
    return p.Writer.Write(bytes.ToUpper(data))
}

接口转换

Go语言中对于基础类型的转换是非常严格的,我们无法将一个int类型的值直接复制给int64类型的变量,但是Go语言对于接口类型的转换则非常灵活。对象和接口之间转换、接口和接口之间的转换都可能是隐式的转换。

    var x io.ReadCloser
    var a io.Reader = x
    var b io.Closer = x

io.ReadCloser均满足io.Reader和io.Closer接口,因此可以直接隐式转换

 
type People interface {
    Speak()
}
 
type Student struct {
    name string
}
 
func (s Student) Speak() { //结构体实现接口的方法
    fmt.Println("hello, golang")
}
 
func demo2(s People) {
    s.Speak()
}
 
func demo1(s1 Student) {
   //将结构体显示转换为接口 前提是该结构体满足接口中的方法
    s2 := People(s1)
    demo2(s2)
}

结构体可以显示地转换为接口,只要结构体满足接口的方法

有时候接口和对象之间转换太灵活了,需要人为地限制这种无意之间的适配。常见的做法时定义一个特殊方法来区分接口。例如在Protobuf中,Message接口就定义了一个特有的ProtoMessage,避免其他类型适配了该接口。

type proto.Message interface {
    Reset()
    String() string
    ProtoMessage() //特殊的方法
}

类型断言

当有一个接口类型的变量时,你能在它之上调用的方法只能是接口定义的。如果你将一个具体类型赋值给接口类型的变量(隐式类型转换),你能使用类型断言来获得具体类型。只有这样你才能调用具体类型定义的(但没有定义在接口中的)方法。

例如 我们定义了一个NoiseMaker

type NoiseMaker interface {
    MakeSound()
}

Dog类型实现了该接口:

type Dog string

func (r Dog) MakeSound() {
    fmt.Println("Dog_MakeSound:", r)
}

//定义一个该类特有的方法
func (r Dog) Walk() {
    fmt.Println("Dog_Walk:", r)
}

将将一个具体Dog类型赋值给NoiseMaker接口类型的变量(隐式类型转换)

var NoiseMaker NoiseMaker = Dog("FFF")
noiseMaker.MakeSound()

在别处调用该NoiseMaker变量时,只知道他是一个接口变量,肯定可以调用MakeSound()这个方法,因为可以有很多类型实现这个接口,必须知道这个变量的具体类型才能够调用具体类特有的方法,因此我们需要通过接口类型断言来判断它到底属于哪个类型。

var dog, ok = noiseMaker.(Dog) 
if ok {
    dog.Walk()
} else {
    fmt.Println("失败")
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值