• Golang 基础二


    十一、接口 (interface)

    11.1 接口

    Go 语言不是一种 “传统” 的面向对象编程语言:它里面没有类和继承的概念。

    但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。

    接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。

    type Namer interface {
        Method1(param_list) return_type
        Method2(param_list) return_type
        ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接口是一种契约,实现类型必须满足它,它描述了类型的行为,规定类型可以做什么。接口彻底将类型能做什么,以及如何做分离开来,使得相同接口的变量在不同的时刻表现出不同的行为,这就是多态的本质。

    编写参数是接口变量的函数,这使得它们更具有普适性、一般性。

    (按照约定,只包含一个方法的)接口的名字由方法名加 er 后缀组成,例如 PrinterReaderWriterLoggerConverter 等等。还有一些不常用的方式(当后缀 er 不合适时),比如 Recoverable,此时接口名以 able 结尾,或者以 I 开头(像 .NETJava 中那样)。

    Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。

    在 Go 语言中接口可以有值,一个未初始化的接口类型的变量或一个 接口值var ai Namerai 是一个多字(multiword)数据结构,它的值是 nil。它本质上是一个指针,虽然不完全是一回事。
    在这里插入图片描述
    接口变量里包含了接收者实例的值和指向对应方法表的指针。

    类型(比如结构体)可以实现某个接口的方法集:这个实现可以描述为,该类型的变量上的每一个具体方法所组成的集合,包含了该接口的方法集。实现了 Namer 接口的类型的变量可以赋值给 ai(即 receiver 的值),方法表指针(method table ptr)就指向了当前的方法实现。

    类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口

    实现某个接口的类型(除了实现接口方法外)可以有其他的方法

    一个类型可以实现多个接口

    接口类型可以指向一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)

    即使接口在类型之后才定义,二者处于不同的包中,被单独编译:只要类型实现了接口中的方法,它就实现了此接口。

    type Shaper interface {
    	Area() float32
    }
    
    type Square struct {
    	side float32
    }
    
    func (sq *Square) Area() float32 {
    	return sq.side * sq.side
    }
    
    type Rectangle struct {
    	length, width float32
    }
    
    func (r Rectangle) Area() float32 {
    	return r.length * r.width
    }
    
    func main() {
    
    	r := Rectangle{5, 3} // Area() of Rectangle needs a value
    	q := &Square{5}      // Area() of Square needs a pointer
    	// shapes := []Shaper{Shaper(r), Shaper(q)}
    	// or shorter
    	shapes := []Shaper{r, q}
    	for n, _ := range shapes {
    		fmt.Println("Shape details: ", shapes[n])
    		fmt.Println("Area of this shape is: ", shapes[n].Area()) //接口实例上调用方法
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在接口实例上调用方法,它使此方法更具有一般性
    这是 多态 的 Go 版本,多态:根据当前的类型选择正确的方法,或者说:同一种类型在不同的实例上似乎表现出不同的行为。

    通过接口产生 更干净更简单更具有扩展性 的代码。在开发中为类型添加新的接口很容易。

    备注

    有的时候,也会以一种稍微不同的方式来使用接口这个词:从某个类型的角度来看,它的接口指的是:它的所有导出方法,只不过没有显式地为这些导出方法额外定一个接口而已。

    11.2 接口嵌套接口(内嵌接口)

    一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。

    type ReadWrite interface {
        Read(b Buffer) bool
        Write(b Buffer) bool
    }
    
    type Lock interface {
        Lock()
        Unlock()
    }
    
    type File interface {
        ReadWrite
        Lock
        Close()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    11.3 类型断言

    一个接口类型的变量 varI 中可以包含任何类型的值,必须有一种方式来检测它的 动态 类型,即运行时在变量中存储的值的实际类型。
    通常我们可以使用 类型断言 来测试在某个时刻 varI 是否包含类型 T 的值:
    1)

    v := varI.(T)       // unchecked type assertion
    
    • 1

    varI 必须是一个接口变量

    类型断言可能是无效的,虽然编译器会尽力检查转换是否有效,但是它不可能预见所有的可能性。如报错:panic: interface conversion: main.Shape is *main.Square, not main.Rectangle
    2)

    if v, ok := varI.(T); ok {  // checked type assertion
        Process(v)
        return
    }
    // varI is not of type T
    
    • 1
    • 2
    • 3
    • 4
    • 5

    如果转换合法,vvarI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,okfalse,也没有运行时错误发生。

    应该总是使用上面的方式来进行类型断言

    多数情况下,我们可能只是想在 if 中测试一下 ok 的值

    if _, ok := varI.(T); ok {
        // ...
    }
    
    • 1
    • 2
    • 3
    type Shape interface {
    	Area() float64
    }
    
    type Square struct {
    	side float64
    }
    
    func (s *Square) Area() float64 {
    	return s.side * s.side
    }
    
    type Rectangle struct {
    	length, width float64
    }
    
    func (r Rectangle) Area() float64 {
    	return r.length * r.width
    }
    
    func main() {
    	r := Rectangle{3, 5}
    	s := &Square{5}
    
    	shapes := []Shape{r, s}
    
    	for i2 := range shapes {
    		fmt.Printf("index: %d\n", i2)
    		// v 为指针(*Square)
    		if v, ok := shapes[i2].(*Square); ok { 
    			fmt.Println("square")  
    		}
    		// v 为 Rectangle 变量
    		if v, ok := shapes[i2].(Rectangle); ok {
    			fmt.Println("square")
    		}
    	}
    	// 判断一个值是否实现了某个接口
    	if shape, ok := shapes[0].(Shape); ok {
    		fmt.Println(shape.Area())
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    类型判断:type-switch

    接口变量的类型也可以使用一种特殊形式的 switch 来检测:type-switch

    	switch v := shapes[i2].(type) {
    	case Rectangle:
    		fmt.Printf("Rectangle: %T, value: %v\n", v, v)
    	case *Square:
    		fmt.Printf("Square: %T, value: %v\n", v, v)
    	case nil:
    		fmt.Printf("nil value\n")
    	default:
    		fmt.Printf("Unexpected type %T\n", v)
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以用 type-switch 进行运行时类型分析,但是在 type-switch 不允许有 fallthrough

    如果仅仅是测试变量的类型,不用它的值,那么就可以不需要赋值语句,比如:

    switch areaIntf.(type) {
    case *Square:
    	// TODO
    case *Circle:
    	// TODO
    ...
    default:
    	// TODO
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在处理来自于外部的、类型未知的数据时,比如解析诸如 JSON 或 XML 编码的数据,类型测试和转换会非常有用。

    测试一个值是否实现了某个接口

    这个是类型断言中的一个特例
    假定 v 是一个值,然后我们想测试它是否实现了 Stringer 接口

    		if shape, ok := shapes[0].(Shape); ok {
    			fmt.Println(shape.Area())
    		}
    
    • 1
    • 2
    • 3

    使用接口使代码更具有普适性。

    11.4 接口方法集

    接口变量中存储的具体值是不可寻址的

    type List []int
    
    func (l List) Len() int {
    	return len(l)
    }
    
    func (l *List) Append(val int) {
    	*l = append(*l, val)
    }
    
    type Appender interface {
    	Append(int)
    }
    
    func CountInto(a Appender, start, end int) {
    	for i := start; i <= end; i++ {
    		a.Append(i)
    	}
    }
    
    type Lener interface {
    	Len() int
    }
    
    func LongEnough(l Lener) bool {
    	return l.Len()*10 > 42
    }
    
    func main() {
    
    	var lst List
    	// compiler error:
    	// cannot use lst (type List) as type Appender in argument to CountInto:
    	//       List does not implement Appender (Append method has pointer receiver)
    	// CountInto(lst, 1, 10)
    	if LongEnough(lst) { // VALID: Identical receiver type
    		fmt.Printf("- lst is long enough\n")
    	}
    
    	// A pointer value
    	plst := new(List)
    	CountInto(plst, 1, 10) // VALID: Identical receiver type
    	if LongEnough(plst) {
    		// VALID: a *List can be dereferenced for the receiver
    		fmt.Printf("- plst is long enough\n")
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    将一个值赋值给一个接口时,编译器会确保所有可能的接口方法都可以在此值上被调用,因此不正确的赋值在编译期就会失败。

    总结

    Go 语言规范定义了接口方法集的调用规则:

    • 类型 *T 的可调用方法集包含接受者为 *TT 的所有方法集
    • 类型 T 的可调用方法集包含接受者为 T 的所有方法
    • 类型 T 的可调用方法集包含接受者为 *T 的方法

    11.5 空接口

    空接口或者最小接口 不包含任何方法,它对实现不做任何要求:

    type Any interface {}
    
    • 1

    任何其他类型都实现了空接口,anyAny 是空接口一个很好的别名或缩写。

    空接口类似 Java/C# 中所有类的基类: Object 类,二者的目标也很相近。

    可以给一个空接口类型的变量 var val interface {} 赋任何类型的值。

    每个 interface {} 变量在内存中占据两个字长:一个用来存储它包含的类型,另一个用来存储它包含的数据或者指向数据的指针。
    在这里插入图片描述

    构建通用类型或包含不同类型变量的数组

    使用空接口

    type Element interface{}
    
    type Vector struct {
    	a []Element
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Vector 中存储的所有元素都是 Element 类型,要得到它们的原始类型(unboxing:拆箱)需要用到类型断言。

    复制数据切片至空接口切片

    类似:

    var dataSlice []myType = FuncReturnSlice() 
    var interfaceSlice []interface{} = dataSlice   //错误
    
    • 1
    • 2

    可惜不能这么做,编译时会出错:cannot use dataSlice (type []myType) as type []interface { } in assignment
    原因是它们俩在内存中的布局是不一样的

    // 必须使用 `for-range` 语句来一个一个显式地赋值
    var interfaceSlice []interface{} = make([]interface{}, len(dataSlice))
    for i, d := range dataSlice {
        interfaceSlice[i] = d
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    结论: interface{} 可以接收任何类型,但是[]interface{} 并不可以接收任何类型的切片

    可是普通类型的切片内存布局是:
    在这里插入图片描述

    通用类型的节点数据结构

    type Node struct {
    	le   *Node
    	data interface{}
    	ri   *Node
    }
    
    func NewNode(left, right *Node) *Node {
    	return &Node{left, nil, right}
    }
    
    func (n *Node) SetData(data interface{}) {
    	n.data = data
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    接口到接口

    一个接口的值可以赋值给另一个接口变量,只要底层类型实现了必要的方法。这个转换是在运行时进行检查的,转换失败会导致一个运行时错误:这是 Go 语言动态的一面,可以拿它和 RubyPython 这些动态语言相比较。

    type any01 interface {
    	Name() string
    }
    
    type any02 interface {
    	Age() int
    }
    
    type type01 struct {
    }
    
    func (t *type01) Name() string {
    	return "hello"
    }
    func (t *type01) Age() int {
    	return 19
    }
    
    func main() {
    	var empty interface{}
    	var a any01
    	var c any02
    
    	fmt.Printf("%T\n", a)
    
    	b := new(type01)
    	empty = b
    	a = empty.(any01)
    	c = a.(any02)
    	c.Age()
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    a 转换为 any02 类型是完全动态的:只要 a 的底层类型(动态类型)定义了 Age 方法这个调用就可以正常运行(译注:若 a 的底层类型未定义 Age 方法,此处类型断言会导致 Age,最佳实践应该为 if mpi, ok := a.(any02); ok { mpi.Age() }

    十二、反射 (reflection)

    12.1 方法和类型的反射

    十三、协程 (goroutine) 与通道 (channel)

    Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是协程 (goroutine) 与通道 (channel)。他们需要语言,编译器,和 runtime 的支持。Go 语言提供的垃圾回收器对并发编程至关重要。

    不要通过共享内存来通信,而通过通信来共享内存。

    通信强制协作。

    13.1 什么是协程

    一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。

    一个并发程序可以在一个处理器或者内核上使用多个线程来执行任务,但是只有同一个程序在某个时间点同时运行在多核或者多处理器上才是真正的并行。

    并行是一种通过使用多处理器以提高速度的能力。所以并发程序可以是并行的,也可以不是。

    公认的,使用多线程的应用难以做到准确,最主要的问题是内存中的数据共享,它们会被多线程以无法预知的方式进行操作,导致一些无法重现或者随机的结果(称作竞态)。

    不要使用全局变量或者共享内存,它们会给你的代码在并发运算的时候带来危险。

    解决之道在于同步不同的线程,对数据加锁,这样同时就只有一个线程可以变更数据。

    Go 更倾向于其他的方式,在诸多比较合适的范式中,有个被称作 Communicating Sequential Processes(顺序通信处理)(CSP, C. Hoare 发明的)还有一个叫做 message passing-model(消息传递)(已经运用在了其他语言中,比如 Erlang)。

    在 Go 中,应用程序并发处理的部分被称作 goroutines(协程),它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射(多路复用,执行于)在他们之上的;协程调度器在 Go 运行时很好的完成了这个工作。

    协程工作在相同的地址空间中:Go 使用 channels 来同步协程

    协程是轻量的,比线程更轻:使用 4K 的栈内存就可以在堆中创建它们。
    存在两种并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序)。Go 的协程和通道理所当然的支持确定性的并发方式(例如通道具有一个 sender 和一个 receiver)

    协程是通过使用关键字 go 调用(执行)一个函数或者方法来实现的(也可以是匿名或者 lambda 函数)。

    协程的栈会根据需要进行伸缩,不出现栈溢出;开发者不需要关心栈的大小。当协程结束的时候,它会静默退出:用来启动这个协程的函数不会得到任何的返回值。

    任何 Go 程序都必须有的 main() 函数也可以看做是一个协程,尽管它并没有通过 go 来启动

    13.2 并发和并行的差异

    并行是一种通过使用多处理器以提高速度的能力。

    往往是,一个设计良好的并发程序在并行方面的表现也非常出色。

    runtime.GOMAXPROCS()

    这会告诉运行时有多少个协程同时执行。

    环境变量 GOMAXPROCS`

    更多的处理器并不意味着性能的线性提升。有这样一个经验法则,对于 n 个核心的情况设置 GOMAXPROCSn-1 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1。
    所以如果在某一时间只有一个协程在执行,不要设置 GOMAXPROCS

    总结:GOMAXPROCS 等同于(并发的)线程数量,在一台核心数多于 1 个的机器上,会尽可能有等同于核心数的线程在并行运行。

  • 相关阅读:
    电脑DLL修复工具,一键解决计算机dll丢失
    linux--gdb的使用
    unity工程参照以及评价
    测试界的飞虎队:测试人才战略——测试行业的精英战略(学习了)
    Inveigh结合DNS v6配合NTLM Relay 的利用
    JavaWeb 项目 --- 博客系统(前后分离)
    【激光SLAM】基于滤波的激光SLAM方法(Grid-based)
    Ubuntu下安装Scala
    华为OD机试 - BOSS的收入 - 回溯(Java 2023 B卷 100分)
    css 写带三角形的对话框,空心的三角形边框
  • 原文地址:https://blog.csdn.net/chinusyan/article/details/126095346