Spiga

Go学习笔记(三):接口

2020-04-12 14:38:51

封装数据与行为

结构体定义

type Employee struct { 
    Id string 
    Name string 
    Age int
}

实例创建及初始化

e := Employee{"0", "Bob", 20}
e1 := Employee{Name: "Mike", Age: 30}
e2 := new(Employee) //注意这⾥返回的引⽤/指针,相当于 e := &Employee{}
e2.Id = “2" //与其他主要编程语⾔的差异:通过实例的指针访问成员不需要使⽤->
e2.Age = 22
e2.Name = “Rose"

行为(方法)定义

//第⼀种定义⽅式在实例对应⽅法被调⽤时,实例的成员会进⾏值复制
func (e Employee) String() string {
    return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
} 
//通常情况下为了避免内存拷⻉我们使⽤第⼆种定义⽅式
func (e *Employee) String() string {
    return fmt.Sprintf("ID:%s/Name:%s/Age:%d", e.Id, e.Name, e.Age)
}
 type Employee struct {
	Id   string
	Name string
	Age  int
}

func (e Employee) String() string { //这里传递的是类型
	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
}

func TestStructOperations(t *testing.T) {
	e := Employee{"0", "Bob", 20}
	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
	t.Log(e.String())   //Address is c000068520 Address is c000068550
}

可以看到上面测试程序调用String方法时传递的是类型,log得到的是2不同的地址,如果改成传递地址呢?

func (e &Employee) String() string {  //这里传递的是地址
	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
}

func TestStructOperations(t *testing.T) {
	e := Employee{"0", "Bob", 20}
	fmt.Printf("Address is %x", unsafe.Pointer(&e.Name))
	t.Log(e.String())   //Address is c000068520 Address is c000068520 
}

这时指向了同一地址,因此证明了实例对应⽅法被调⽤时,实例的成员会进⾏值复制。为了避免内存拷⻉我们使⽤第⼆种定义⽅式。

接口

Go语言采用Duck Type 式接⼝实现,与其他主要编程语⾔的差异

  • 接⼝为⾮⼊侵性,实现不依赖于借⼝定义
  • 所以接⼝的定义可以包含在接⼝使⽤者包内。(不会出现其他语言的循环依赖问题,如下图为其他语言打包2个包时出现的循环依赖)
type Programmer interface {
	WriteHelloWorld() string
}

type GoProgrammer struct {
}

func (g *GoProgrammer) WriteHelloWorld() string {
	return "fmt.Println(\"Hello World\")"
}

func TestClient(t *testing.T) {
	var p Programmer
	p = new(GoProgrammer)
	t.Log(p.WriteHelloWorld())
}

接口变量

自定义类型

type IntConvertionFn func(n int) int
type MyPoint int

自定义类型可以让程序更具可读性,如之前介绍过的计算函数执行时长的函数,该函数因为传入和传出都是一个函数,程序阅读起来不太清晰,改用自定义类型后就容易理解多了,如下:

// 原函数
func timeSpent(inner func(op int) int) func(op int) int {
    return func(n int) int {
        start := time.Now()
        ret := inner(n)
        fmt.Println("time spent:", time.Since(start).Seconds())
        return ret
    }
}

// 用自定义类型改造后
type IntConv func(op int) int
func timeSpent(inner IntConv) IntConv {
    return func(n int) int {
        start := time.Now()
        ret := inner(n)
        fmt.Println("time spent:", time.Since(start).Seconds())
        return ret
    }
}

扩展与复合

其他主要编程语⾔的差异

Go 不⽀持继承,但可以通过复合的⽅式来复⽤

匿名类型嵌⼊

与其他主要编程语⾔的差异

它不是继承,如果我们把“内部 struct ”看作⽗类,把“外部 struct” 看作⼦类,会发现如下问题:

  • 不⽀持⼦类替换
  • ⼦类并不是真正继承了⽗类的⽅法
    • ⽗类的定义的⽅法⽆法访问⼦类的数据和⽅法
type Pet struct {
}

func (p *Pet) Speak() {
    fmt.Print("...")
}

func (p *Pet) SpeakTo(host string) {
    p.Speak()
    fmt.Println(" ", host)
}

type Dog struct {
    Pet
}

func (d *Dog) Speak() {
    fmt.Print("Wang!")
}

func TestDog(t *testing.T) {
    dog := new(Dog)
    dog.SpeakTo("Chao")
    // 测试返回结果 ...  Chao
}

如上代码Dog结构体中通过匿名类型嵌入了Pet,然后可以调用Pet中的方法,但Pet中的方法Speak是⽆法访问的Dag中的Speak⽅法的。同样在TestDog测试方法中,我们无法做到 Dog类型到Pet类型的转换

多态

Map声明

如下图Go语言中多态的实现

type Code string
type Programmer interface {
    WriteHelloWorld() Code
}

type GoProgrammer struct {
}

func (p *GoProgrammer) WriteHelloWorld() Code {
    return "fmt.Println(\"Hello World!\")"
}

type JavaProgrammer struct {
}

func (p *JavaProgrammer) WriteHelloWorld() Code {
    return "System.out.Println(\"Hello World!\")"
}

func writeFirstProgram(p Programmer) {
    fmt.Printf("%T %v\n", p, p.WriteHelloWorld())
}

func TestPolymorphism(t *testing.T) {
    goProg := &GoProgrammer{}
    javaProg := new(JavaProgrammer)
    writeFirstProgram(goProg)
    writeFirstProgram(javaProg)
    // 测试结果
    // *polymorphism.GoProgrammer fmt.Println("Hello World!")
    // *polymorphism.JavaProgrammer System.out.Println("Hello World!")
}

空接⼝与断⾔

  1. 空接⼝可以表示任何类型
  2. 通过断⾔来将空接⼝转换为制定类型
    v, ok := p.(int) // ok=true 时为转换成功

断言实例

func DoSomething(p interface{}) {
    // 用if来断言
    if i, ok := p.(int); ok {
        fmt.Println("Integer", i)
        return
    }
    if s, ok := p.(string); ok {
        fmt.Println("stirng", s)
        return
    }
    fmt.Println("Unknow Type")

    // 用switch来断言
    switch v := p.(type) {
        case int:
            fmt.Println("Integer", v)
        case string:
            fmt.Println("String", v)
        default:
            fmt.Println("Unknow Type")
    }
}

上面代码我们可以看到实际使用时用switch来做多断言判断更简便。

Go 接⼝最佳实践

  1. 倾向于使⽤⼩的接⼝定义,很多接⼝只包含⼀个⽅法
    type Reader interface { 
        Read(p []byte) (n int, err error)
    }
    type Writer interface { 
        Write(p []byte) (n int, err error)
    }
    
  2. 较⼤的接⼝定义,可以由多个⼩接⼝定义组合⽽成
    type ReadWriter interface { 
        Reader 
        Writer
    }
    
  3. 只依赖于必要功能的最⼩接⼝
    func StoreData(reader Reader) error { 
        …
    }
    

Go的错误机制

  1. 没有异常机制
  2. error 类型实现了 error 接⼝
    type error interface { 
        Error() string
    }
    
  3. 可以通过 errors.New 来快速创建错误实例
    errors.New("n must be in the range [0,100]")
    

最佳实践原则(及早失败,避免嵌套!)

// 定义不同的错误变量,以便于判断错误类型
var LessThanTwoError error = errors.New("n must be greater than 2")
var GreaterThanHundredError error = errors.New("n must be less than 100")

func GetFibonacci(n int) ([]int, error) {
    if n < 2 {
        return nil, LessThanTwoError
    }
    if n > 100 {
        return nil, LargerThenHundredError
    }
    fibList := []int{1, 1}

    for i := 2; /*短变量声明 := */ i < n; i++ {
        fibList = append(fibList, fibList[i-2]+fibList[i-1])
    }
    return fibList, nil
}

panic

  • panic ⽤于不可以恢复的错误
  • panic 退出前会执⾏ defer 指定的内容

panic vs. os.Exit

  • os.Exit 退出时不会调⽤ defer 指定的函数
  • os.Exit 退出时不输出当前调⽤栈信息

recover

defer func() {
    if err := recover(); err != nil { 
        //恢复错误  
    }
}()

**最常⻅的”错误恢复“ **

defer func() {
    if err := recover(); err != nil { 
        log.Error(“recovered panic”,err)    
    }
}()

当心! recover 成为恶魔

  • 形成僵⼫服务进程,导致 health check 失效。
  • “Let it Crash!” 往往是我们恢复不确定性错误的最好⽅法。