Go语言面向接口

第一章 面向接口

image

1.1 接口的概念

现有infra和testing两个包都有Retriever这个结构体,都有Get函数绑定了Retriever类型。

infra包下的urlretriever.go文件代码:

package infra

import (
   "io/ioutil"
   "net/http"
)

type Retriever struct{}//infra下的Retriever结构体

func (Retriever) Get(url string) string { //给Retriever类型绑定一个Get方法,实现接口中的Get方法
   resp, err := http.Get(url)
   if err != nil {
      panic(err)
   }

   defer resp.Body.Close()

   bytes, _ := ioutil.ReadAll(resp.Body)
   return string(bytes)
}

testing包下的Retriever.go文件代码:

package testing

type Retriever struct{}//testing下的Retriever结构体

func (Retriever) Get(url string) string { //给Retriever类型绑定一个Get方法,实现接口中的Get方法
   return "fake content"
}

含有main函数的downloader.go文件代码:

package main

import (
   "fmt"
   "learngo/infra"
)

func getRetriever() retriever {
   return infra.Retriever{} //想要更改不同包的Retriever只用修改这里的infra即可
}

type retriever interface { //定义了接口后就无需再关心不同包下相同名字的类型到底是哪个包的了
   Get(string) string //无论是哪个包的Retriever现在都可以通过retriever这个接口来调用Get了
}

func main() {
   var r retriever = getRetriever()            //定义了接口后就不用知道r到底是infra下的还是testing下的Retriever了
   fmt.Println(r.Get("https://www.imooc.com")) //通过retriever这个接口来调用Get
}

1.2 duck typing的概念

duck typing:如果一个物体,它长得像“”鸭子”,叫起来像鸭子” 那么我就认为它是鸭子。

鸭子 通常定义为“形体为黄色,会浮在水面叫声为嘎嘎嘎的家禽”
但是“我”(假设我是小孩子)不这么认为,我认为 大黄鸭就是鸭子,它是黄色的,可以漂浮,会嘎嘎的叫。
再比如“我”(假设我是个小公举)我认为 “玻尿酸鸭”,就是真正的鸭子,它也是黄色的,会叫。

duck typing 实际意思就是以“使用者”的视角去定义, 只要使用者觉得OK,这就是我眼中的鸭子,那就好。而不需要定义者去限定到底什么才是“鸭子”

  • duck typing意为描述事物的外部行为而非内部结构
  • 严格来说go属于结构化类型系统,类似duck typing

1.3 接口的定义和实现

image

  • 接口由使用者定义
  • 实现者只管实现接口中的功能
  • 实现者必须要实现接口中的所有方法

1.3.1 为什么要让子类继承父类接口?

接口相当于父类,实现者相当于子类,子类要通过继承父类接口来实现接口中的方法,且必须实现接口中的所有方法

光标在子类的类名中停留会弹出灯泡,点击后会出现“implement interface”(或者直接在类名处停留输入Alt+Enter也会出现“implement interface”),点击后即可搜索想要实现的接口。

那么让子类继承父类接口的目的是什么?

答:假如一个子类Cat实现了接口Animal的方法(Animal接口中有且只有一个Eat()方法),那么就可以声明Animal接口类型指向Cat实例(即多态),之后可以直接通过Cat实例来调用实现了Animal接口中的Eat()方法。

image

✳✳✳【精髓】接口的本质是指针✳✳✳

1.3.2 Golang中面向对象多态的实现

通过定义接口当作父类,再让子类继承父类接口实现多态,但在go中,子类继承父类接口在其结构体内无需像一个结构体继承另一个结构体时,要在子类结构体内写上父类的类名,子类继承父类接口时不需要在其结构体内写父类接口的名字,只需要实现接口中的方法即可。

【精髓】因为接口类型的本质就是指针,所以可以说多态就是父类类型的变量(即指针类型)指向子类的具体数据(即实例)。

AnimalIF例子:

Cat和Dog两个类继承了AnimalIF父类接口。

package main

import "fmt"

//父类接口AnimalIF
//接口本质是一个指针
type AnimalIF interface {
   Sleep()
   GetColor() string
   GetType() string
}

//Cat部分===================================================================================
type Cat struct {
   color string
}

func (c *Cat) Sleep() {
   fmt.Println("A cat is sleeping")
}

func (c *Cat) GetColor() string {
   return c.color
}

func (c *Cat) GetType() string {
   return "Cat"
}

//Cat部分===================================================================================

//Dog部分===================================================================================
type Dog struct {
   color string
}

func (d *Dog) Sleep() {
   fmt.Println("A dog is sleeping")
}

func (d *Dog) GetColor() string {
   return d.color
}

func (d *Dog) GetType() string {
   return "Dog"
}

//Dog部分===================================================================================

//接口类型的函数showAnimal(),这里要传入的是一个地址,而AnimalIF类型就是指针类型
func showAnimal(a AnimalIF) {
   a.Sleep()
   fmt.Println("color is", a.GetColor())
   fmt.Println("type is", a.GetType())
}

func main() {
   /*
      var animal AnimalIF

      animal = &Cat{"White"}         //将Cat类型赋值给接口类型AnimalIF实现多态
      animal.Sleep()                 //调用的是Cat的Sleep()
      fmt.Println(animal.GetColor()) //调用的是Cat的GetColor()
      fmt.Println(animal.GetType())  //调用的是Cat的GetType()

      //将Dog类型赋值给接口类型AnimalIF实现多态
      animal = &Dog{"Black"}
      animal.Sleep()                 //调用的是Dog的Sleep()
      fmt.Println(animal.GetColor()) //调用的是Dog的GetColor()
      fmt.Println(animal.GetType())  //调用的是Dog的GetType()
   */

   var animal AnimalIF
   animal = &Cat{"blue"} //animal是接口类型,也就是指针类型,所以这里要加&
   showAnimal(animal)    //animal是接口类型,也就是指针类型所以这里不用加&直接可以传入animal进去
   /*
      控制台输出:
      A cat is sleeping
      color is blue
      type is Cat
   */

   cat := Cat{"yellow"} //cat是Cat类型,如果是cat := &Cat{"yellow"}那么cat就是指向Cat类的指针类型
   showAnimal(&cat)     //cat不是地址,所以要加&才能传入showAnimal()
   /*
      控制台输出:
      A cat is sleeping
      color is yellow
      type is Cat
   */
}

1.4 接口的值的类型(空接口万能类型与类型断言机制)

1.4.1 接口里有什么?

接口变量里有两个东西,一个是类型一个是值。

  1. 接口变量里两个值分别是 实现者的类型实现者的值

image

  1. 接口变量里两个值分别是 实现者的类型实现者的指针(实现者的指针指向实现者)

在实际的应用中注意不要使用一个接口变量的地址,因为接口本身就可以含有一个指针,让其内部指向一个实现者就可以了。

image

代码示例:

image

mockretriever.go

package mock

type Retriever struct { //假的Retriever
   Contents string
}

//mock.Retriever实现Retriever接口中的Get方法
//值接收者
func (r Retriever) Get(url string) string {
   return r.Contents
}

retriever.go

package real

import (
   "net/http"
   "net/http/httputil"
   "time"
)

type Retriever struct { //真的Retriever
   UserAgent string
   TimeOut   time.Duration
}

//real.Retriever实现Retriever接口中的Get方法
//指针接收者
func (r *Retriever) Get(url string) string {
   resp, err := http.Get(url)
   if err != nil {
      panic(err)
   }

   result, err := httputil.DumpResponse(resp, true)

   resp.Body.Close()

   if err != nil {
      panic(err)
   }

   return string(result)
}

main.go

package main

import (
   "fmt"
   "learngo/retriever/mock"
   "learngo/retriever/real"
   "time"
)

type Retriever interface {
   Get(url string) string
}

//传入参数为接口类型Retriever的download函数
func download(r Retriever) string {
   return r.Get("http://www.imooc.com")
    //意为将结构体赋值给接口变量后,接口变量就可以调用结构体中实现了接口中的方法
    //详细到此处的话,就是接口相当于一个模板,Retriever接口中有Get方法,比如real.Retriever这个结构体有一个绑定real.Retriever类型的Get方法,该方法就称作为实现了Retriever接口中的Get方法
    //将real.Retriever赋值给Retriever接口类型的实例r后,r就可以调用real.Retriever中的Get方法
}

func main() {
   //1. 接口变量里两个值分别是 实现者的类型 和 实现者的值
   var r Retriever
   r = mock.Retriever{"this is a fake imooc.com"} //可以把一个结构体类型赋值给接口,实现多态
   //这里也可以r = &mock.Retriever{"this is a fake imooc.com"}
   //因为指针接收者只能以指针方式使用,但是值接收者可以以值方式也可以以指针方式使用
   //但是r就变成了*mock.Retriever类型
   inspect(r)
   //控制台输出:
   //接口r的类型为:mock.Retriever,接口r的值为:{this is a fake imooc.com}
   //Contents: this is a fake imooc.com

   //2. 接口变量里两个值分别是 实现者的类型 和 实现者的指针(实现者的指针指向实现者)
   r = &real.Retriever{ //这里必须加&,因为r是接口类型,必须赋值给一个地址
      UserAgent: "Mozilla/5.0",
      TimeOut:   time.Minute,
   }
   inspect(r)
   //控制台输出:
   //接口r的类型为:*real.Retriever,接口r的值为:&{Mozilla/5.0 1m0s}
   //UserAgent: Mozilla/5.0

   //来判断接口肚子里值的类型的方法其二:Type Assertion(类型断言)
   //通过将r.(类型的名字)赋值给一个变量
   //类型断言是一个作用在接口值上的操作,写出来类似于x.(T),其中x是一个接口类型的表达式,而T是一个类型(称为断言类型)。类型断言会检查作为操作数的动态类型是否满足指定的断言类型,通过这种形式判断当前接口指向的是何种数据类型
   
    mockRetriever, ok := r.(mock.Retriever)
    //当mock.Retriever在实现Retriever的Get()方法时传递的是指针类型时,就必须是r.(*mock.Retriever)
    //如果不加*会报错:
	//mock.Retriever does not implement Retriever (Get method has pointer receiver)
    //但此时mock.Retriever在实现Retriever的Get()方法时传递的是值类型,所以可以加*也可以不加
   if ok {
      fmt.Println(mockRetriever.Contents) //如果r是mock.Retriever类型就打印里面的Contents
   } else {
      fmt.Println("not a mock retriever") //如果r不是mock.Retriever类型就打印not a mock retriever
   }
    //由于r不是mock.Retriever类型,所以控制台输出:not a mock retriever
}

//来判断接口肚子里值的类型的方法其一:
//用switch来揭示接口Retriever肚子里数据的类型
func inspect(r Retriever) {
   fmt.Printf("接口r的类型为:%T,接口r的值为:%v\n", r, r)
   switch v := r.(type) {
   case mock.Retriever:
      fmt.Println("Contents:", v.Contents)
   case *real.Retriever:
      fmt.Println("UserAgent:", v.UserAgent)
   }
}

【小结】

  • 接口变量自带指针
  • 接口变量同样采用值传递,几乎不需要使用接口的指针
  • 【精髓】指针接收者只能以指针方式使用(即传入的参数必须是指针类型),但是值接收者可以以值方式也可以以指针方式使用(即传入的参数可以是值类型也可以是指针类型),这句话会多次在打码的时候应证

1.4.2 查看接口变量

  • 表示任何类型:interface{}

    • 没有任何方法的接口就是空接口,实际上每个类型都实现了空接口,所以空接口类型可以接受任何类型的数据。
  • 利用case查看接口变量类型

  • Type Assertion

空接口interface{}示例:

用到了第四章 面向“对象”4.3.2 定义别名中stack的例子,将Stack从[]int类型改为interface{}类型后栈内就能存储不限于int的任意类型元素。

stack.go

package stack

type Stack []interface{}

func (s *Stack) Push(v interface{}) {
   *s = append(*s, v)
}

func (s *Stack) Pop() interface{} {
   back := (*s)[len(*s)-1]
   *s = (*s)[:len(*s)-1]
   return back //注意不要直接返回(*s)[len(*s)-1]而省略back := (*s)[len(*s)-1],这样会返回出栈之后栈内最后一个元素
}

func (s *Stack) IsEmpty() bool {
   return len(*s) == 0 //注意不要返回*s == nil,就算切片没内容也不会是nil
}

1.5 接口的组合

可以在一个接口中嵌入多种小接口,这样这个接口就能调用成员接口中的方法。

示例:

mockretriever.go

package mock

type Retriever struct { //假的Retriever
   Contents string
}

//实现的方法接收者类型最好保持一致
//这里的Post和Get都是指针接收者,这样才能修改form["contents"]的值
//实现接口中的Post方法
func (r *Retriever) Post(url string, form map[string]string) string {
   r.Contents = form["contents"]
   return "ok"
}

//实现接口中的Get方法
func (r *Retriever) Get(url string) string {
   return r.Contents
}

main.go

package main

import (
   "fmt"
   "learngo/retriever/mock"
)

type Retriever interface {
   Get(url string) string
}

type Poster interface {
   Post(url string, form map[string]string) string
}

const url = "http://www.imooc.com"

func post(poster Poster) {
   poster.Post(url, map[string]string{
      "name":   "liam",
      "course": "golang",
   })
}

type RetrieverPoster interface {
   //组合接口
   Retriever
   Poster
   //也可以在组合接口中定义一些方法
   //比如connect(host string)
}

//由Retriever和Poster组合的RetrieverPoster接口变量可以调用Retriever和Poster的方法
func session(s RetrieverPoster) string {
   s.Post(url, map[string]string{"contents": "another faked imooc.com"})
   return s.Get(url)
}

func main() {
   retriever := mock.Retriever{"this is a fake imooc.com"}
   fmt.Println("Try a session")
   fmt.Println(session(&retriever))
    //因为mock.Retriever同时实现了Get和Post方法,所以可以将mock.Retriever类型传入至session中
}

1.6 常用系统接口

1.6.1 Stringer接口

在fmt包下的print.go中的Stringer接口:

// Stringer is implemented by any value that has a String method,
// which defines the ``native'' format for that value.
// The String method is used to print values passed as an operand
// to any format that accepts a string or to an unformatted printer
// such as Print.
type Stringer interface {
   String() string
}

这个接口类似于Java中的重写toString()方法,目的是为了直接打印变量时会打印出我们期望的字符串而不是输出默认的字符串,提高了阅读性。

mock.Retriever中实现Stringer接口:

package mock

import "fmt"

type Retriever struct {
   Contents string
}

func (r *Retriever) String() string {
   return fmt.Sprintf(
      "Retriever:{Contents=%s}", r.Contents)
}

func (r *Retriever) Get(url string) string {
   return r.Contents
}

main:

package main

import (
   "fmt"
   "learngo/retriever/mock"
)

type Retriever interface {
   Get(url string) string
}

func main() {
   mr := &mock.Retriever{"im a faker"}
   fmt.Println(mr)
}

实现Stringer接口前的控制台输出:&{im a faker}

实现Stringer接口后的控制台输出:Retriever:{Contents=im a faker}

【值得注意的是】当实现Stringer接口中的String()方法时:

  • 若String()的接收者是指针类型,那么只有当输出对象是指针类型的值时才会输出期望输出的格式,因为实现接口的类型是指针类型
  • 若String()的接收者是值类型,那么当输出对象是值类型和指针类型的值都可以输出期望输出的格式,因为指针接收者只能以指针方式使用(即传入的参数必须是指针类型),但是值接收者可以以值方式也可以以指针方式使用(即传入的参数可以是值类型也可以是指针类型)

1.6.2 Reader接口、Writer接口

Reader接口(in io/io.go):

type Reader interface {
   Read(p []byte) (n int, err error)
}

其功能是把一个文件读取到内存p中

在之前的第二章Go基础语法中的2.5循环语句中的读取文件例子中,可以解读出一些信息:

其中abc.txt内容如下:

image

package main

import (
   "bufio"
   "fmt"
   "os"
)

func printFile(filename string) {
   file, err := os.Open(filename) //os.Open()传入string类型返回*File类型赋值给file
   if err != nil {
      panic(err)
   }

   scanner := bufio.NewScanner(file)
   //bufio.NewScanner()需要传入的是io.Reader接口类型
   /*
      但这里传入的file是*File类型,因为*File类型实现了Reader接口的read()方法
      相当于*File子类类型继承了Reader父类接口,所以子类类型可以传入需要父类类型接收者的方法
   */

   for scanner.Scan() {
      fmt.Println(scanner.Text())
   }
}

func main() {
   printFile("abc.txt")
}

printFile()打印的是一个文件名,若将传入的参数类型换成io.Reader接口类型那么扩展性会更好。

将参数类型换成io.Reader接口类型后:

package main

import (
   "bufio"
   "fmt"
   "io"
   "os"
   "strings"
)

func printFile(filename string) {
   file, err := os.Open(filename)
   if err != nil {
      panic(err)
   }
   printFileContents(file)
}

func printFileContents(reader io.Reader) {
   scanner := bufio.NewScanner(reader)

   for scanner.Scan() {
      fmt.Println(scanner.Text())
   }
}

func main() {
   printFile("loop/abc.txt")
   s := `abcd"gy"
   s
   1256
   kksw2ep` //注意这里的``不是单引号,单引号是‘’,``可以用来表示跨行的字符串
   printFileContents(strings.NewReader(s))
}

io.Reader接口类型作为参数类型后,就不止能打印文件内的信息,也可以打印字符串,控制台输出如下:

hello
say hello again
abcd
        s
        1256
        kksw2ep
posted @   雪碧锅仔饭  阅读(152)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示