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!")
}
空接⼝与断⾔
- 空接⼝可以表示任何类型
- 通过断⾔来将空接⼝转换为制定类型
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 接⼝最佳实践
- 倾向于使⽤⼩的接⼝定义,很多接⼝只包含⼀个⽅法
type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) }
- 较⼤的接⼝定义,可以由多个⼩接⼝定义组合⽽成
type ReadWriter interface { Reader Writer }
- 只依赖于必要功能的最⼩接⼝
func StoreData(reader Reader) error { … }
Go的错误机制
- 没有异常机制
- error 类型实现了 error 接⼝
type error interface { Error() string }
- 可以通过 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!” 往往是我们恢复不确定性错误的最好⽅法。