다음 튜토리얼 칼럼인 golang에서는 golang의 인터페이스 인터페이스에 대한 심층 분석을 제공할 예정입니다. 도움이 필요한 친구들에게 도움이 되길 바랍니다!
인터페이스 소개
고룬틴과 채널이 Go 언어의 동시성 모델을 지원하고 Go 언어를 오늘날의 클러스터링 및 멀티 코어 시대에 아름다운 풍경으로 만드는 초석이라면, 인터페이스는 Go입니다. 이는 Go 언어가 기본 프로그래밍 철학 탐구에서 전례 없는 수준에 도달할 수 있도록 하는 전체 언어 유형 시리즈의 초석입니다. Go 언어는 프로그래밍 철학에 있어서 개량주의자라기보다 개량주의자입니다. 이는 Go 언어에 고루틴과 채널이 있기 때문이 아니라 더 중요한 것은 Go 언어의 유형 시스템 때문이며, 더욱이 Go 언어의 인터페이스 때문입니다. Go 언어의 프로그래밍 철학은 인터페이스 때문에 완벽해지는 경향이 있습니다. C++ 및 Java는 "침입" 인터페이스를 사용합니다. 그 이유는 구현 클래스가 특정 인터페이스를 구현한다는 것을 명시적으로 선언해야 하기 때문입니다. 이 필수 인터페이스 상속 방법은 객체 지향 프로그래밍 아이디어 개발에 있어 상당한 의심을 받아온 기능입니다. Go 언어는 "비침입적 인터페이스"를 사용합니다. Go 언어의 인터페이스는 고유한 특성을 가지고 있습니다. T 유형의 공개 메소드가 인터페이스 I의 요구 사항을 완전히 충족하는 한 인터페이스 I이 필요한 곳에 T 유형의 객체를 사용할 수 있습니다. 소위 T 유형의 공개 메소드는 인터페이스 I의 요구사항을 완전히 충족합니다. 즉, T 유형은 인터페이스 I에 지정된 멤버 세트를 구현합니다. 이 접근 방식의 학명은 Structural Typing이며, 어떤 사람들은 이를 일종의 정적 Duck Typing으로 간주하기도 합니다.
이 값은 인터페이스 메소드를 구현해야 합니다.
type Reader interface { Read(p []byte) (n int, err os.Error) } // Writer 是包裹了基础 Write 方法的接口。 type Writer interface { Write(p []byte) (n int, err os.Error) } var r io.Reader r = os.Stdin r = bufio.NewReader(r) r = new(bytes.Buffer)
한 가지 분명한 점은 r이 어떤 값을 저장하든 r의 유형은 항상 io.Reader
이고, r의 정적 유형은 io.Reader입니다. 인터페이스 유형의 매우 중요한 예는 빈 인터페이스 인터페이스{}입니다. 이는 빈 메서드 집합을 나타냅니다. 모든 값에는 0개 이상의 메서드가 있으므로 모든 값이 이를 충족할 수 있습니다. 어떤 사람들은 Go의 인터페이스가 동적으로 입력된다고 말하지만 이는 오해입니다. 정적으로 유형이 지정됩니다. 인터페이스 유형의 변수는 항상 동일한 정적 유형을 가지며 값은 항상 빈 인터페이스를 충족하지만 인터페이스 변수에 저장된 값은 런타임 시 변경될 수 있습니다. 리플렉션과 인터페이스는 밀접하게 연관되어 있으므로 이 모든 것은 주의해서 다루어야 합니다. io.Reader
,Go 是静态类型,而 r 的静态类型是 io.Reader。接口类型的一个极端重要的例子是空接口interface{},它表示空的方法集合,由于任何值都有零个或者多个方法,所以任何值都可以满足它。也有人说 Go 的接口是动态类型的,不过这是一种误解。 它们是静态类型的:接口类型的变量总是有着相同的静态类型,这个值总是满足空接口,只是存储在接口变量中的值运行时可能被改变。对于所有这些都必须严谨的对待,因为反射和接口密切相关。
二 接口类型内存布局
在类型中有一个重要的类别就是接口类型,表达了固定的一个方法集合。一个接口变量可以存储任意实际值(非接口),只要这个值实现了接口的方法。interface在内存上实际由两个成员组成,如下图,tab指向虚表,data则指向实际引用的数据。虚表描绘了实际的类型信息及该接口所需要的方法集。
type Stringer interface { String() string } type Binary uint64 func (i Binary) String() string { return strconv.FormatUint(i.Get(), 2) } func (i Binary) Get() uint64 { return uint64(i) } func main() { var b Binary = 32 s := Stringer(b) fmt.Print(s.String()) }
观察itable的结构,首先是描述type信息的一些元数据,然后是满足Stringger接口的函数指针列表(注意,这里不是实际类型Binary的函数指针集哦)。因此我们如果通过接口进行函数调用,实际的操作其实就是s.tab->fun[0](s.data)
。是不是和C++的虚表很像?但是他们有本质的区别。先看C++,它为每个类创建了一个方法集即虚表,当子类重写父类的虚函数时,就将表中的相应函数指针改为子类自己实现的函数,如果没有则指向父类的实现,当面临多继承时,C++对象结构里就会存在多个虚表指针,每个虚表指针指向该方法集的不同部分。我们再来看golang的实现方式,同C++一样,golang也为每种类型创建了一个方法集,不同的是接口的虚表是在运行时专门生成的,而c++的虚表是在编译时生成的(但是c++虚函数表表现出的多态是在运行时决定的).例如,当例子中当首次遇见s := Stringer(b)
//t := []int{1, 2, 3, 4} wrong //var s []interface{} = t t := []int{1, 2, 3, 4} //right s := make([]interface{}, len(t)) for i, v := range t { s[i] = v }
🎜🎜🎜그것을 관찰하세요 구조는 먼저 유형 정보를 설명하는 일부 메타데이터이고 그 다음에는 Stringger 인터페이스를 만족하는 함수 포인터 목록입니다(이것은 실제 유형 Binary의 함수 포인터 세트가 아님에 유의하십시오). 따라서 인터페이스를 통해 함수 호출을 하면 실제로는 s.tab->fun[0](s.data)
가 됩니다. C++의 가상 테이블과 비슷합니까? 그러나 그것들은 근본적으로 다릅니다. 먼저 C++를 살펴보겠습니다. 즉, 각 클래스에 대한 가상 테이블인 메서드 세트를 생성합니다. 하위 클래스가 상위 클래스의 가상 함수를 재정의하면 테이블의 해당 함수 포인터가 하위 클래스에 의해 구현된 함수로 변경됩니다. 그렇지 않은 경우 다중 상속에 직면할 때 상위 클래스의 구현을 가리키면 C++ 개체 구조에 여러 개의 가상 테이블 포인터가 있고 각 가상 테이블 포인터는 메서드 집합의 다른 부분을 가리킵니다. golang의 구현을 살펴보겠습니다. C++과 마찬가지로 golang도 각 유형에 대한 메소드 세트를 생성합니다. 차이점은 인터페이스의 가상 테이블은 런타임에 특별히 생성되는 반면 C++의 가상 테이블은 컴파일 타임에 생성됩니다. 그러나 C++ 가상 함수 테이블에 의해 표시되는 다형성은 런타임에 결정됩니다. 예를 들어, 예제에서 처음으로 s := Stringer(b)
와 같은 문이 나타나면 golang은 다음을 생성합니다. Stringer 인터페이스는 Binary 형태의 가상 테이블에 해당하며 이를 캐시합니다. 그렇다면 왜 C++를 사용하여 구현하지 않는 걸까요? 이 C++ 및 Golang 개체 메모리 레이아웃은 서로 관련되어 있습니다. 🎜🎜
首先c++的动态多态是以继承为基础的,在对象构造初始化的时首先会初始化父类,其次是子类,也就是说一个对象的内存布局是虚表,父类部分,子类部分(编译器不同可能会有差异),当一个父类指针指向子类时,会发生内存的截断,截断子类部分(内存地址偏移),但是此时子类的虚表中的函数指针实际上还是指向了自己的实现,所以此时的指针才会调用到子类的虚函数,如果不是虚函数,因为内存已经截断没有子类的非虚函数信息了,所以只能调用父类的了,这种继承关系让c++的虚表的初始化非常清晰,在一个对象初始化时先调用父类的构造此时虚表跟父类是一样的,接下来初始化子类,此时编译器就会去识别子类有没有覆盖父类的虚函数,如果有则虚表中相应的函数指针改成自己的虚函数实现指针。
那么go有什么不同呢,首先我们很清楚go是没有严格意义上的继承的,go的接口不存在继承关系,只要实现了接口定义的方法都可以成为接口类型,这给go的虚表初始化带来很大的麻烦,到底有多少类型实现了这个接口,一个类型到底实现了多少接口这让编译器很confused。举个例子,某个类型有m个方法,某接口有n个方法,则很容易知道这种判定的时间复杂度为O(mXn),不过可以使用预先排序的方式进行优化,实际的时间复杂度为O(m+n)这样看来其实还行那为什么要在运行时生成虚表呢,这不是会拖慢程序的运行速度吗,注意我们这里是某个类型,某个接口,是1对1的关系,如果有n个类型,n个接口呢,编译器难道要把之间所有的关系都理清吗?退一步说就算编译器任劳任怨把这事干了,可是你在写过程中你本来就不想实现那个接口,而你无意中给这个类型实现的方法中包含了某些接口的方法,你根本不需要这个接口(况且go的接口机制会导致很多这种无意义的接口实现),你欺负编译器就行了,这也太欺负人了吧。如果我们放到运行时呢,我们只要在需要接口的去分析一下类型是否实现了接口的所有方法就行了很简单的一件事。
三 空接口
接口类型的一个极端重要的例子是空接口:interface{}
,它表示空的方法集合,由于任何值都有零个或者多个方法,所以任何值都可以满足它。 注意,[]T不能直接赋值给[]interface{}
//t := []int{1, 2, 3, 4} wrong //var s []interface{} = t t := []int{1, 2, 3, 4} //right s := make([]interface{}, len(t)) for i, v := range t { s[i] = v }
str, ok := value.(string) if ok { fmt.Printf("string value is: %q\n", str) } else { fmt.Printf("value is not a string\n") }
在Go语言中,我们可以使用type switch
语句查询接口变量的真实数据类型,语法如下:
type Stringer interface { String() string } var value interface{} // Value provided by caller. switch str := value.(type) { case string: return str //type of str is string case Stringer: //type of str is Stringer return str.String() }
也可以使用“comma, ok”的习惯用法来安全地测试值是否为一个字符串:
str, ok := value.(string) if ok { fmt.Printf("string value is: %q\n", str) } else { fmt.Printf("value is not a string\n") }
四 接口赋值
package main import ( "fmt" ) type LesssAdder interface { Less(b Integer) bool Add(b Integer) } type Integer int func (a Integer) Less(b Integer) bool { return a < b } func (a *Integer) Add(b Integer) { *a += b } func main() { var a Integer = 1 var b LesssAdder = &a fmt.Println(b) //var c LesssAdder = a //Error:Integer does not implement LesssAdder //(Add method has pointer receiver) }
go语言可以根据下面的函数:
func (a Integer) Less(b Integer) bool
自动生成一个新的Less()方法
func (a *Integer) Less(b Integer) bool
这样,类型*Integer就既存在Less()
方法,也存在Add()
方法,满足LessAdder接口。 而根据
func (a *Integer) Add(b Integer)
这个函数无法生成以下成员方法:
func(a Integer) Add(b Integer) { (&a).Add(b) }
因为(&a).Add()
改变的只是函数参数a,对外部实际要操作的对象并无影响(值传递),这不符合用户的预期。所以Go语言不会自动为其生成该函数。因此类型Integer只存在Less()方法,缺少Add()
方法,不满足LessAddr接口。(可以这样去理解:指针类型的对象函数是可读可写的,非指针类型的对象函数是只读的)将一个接口赋值给另外一个接口 在Go语言中,只要两个接口拥有相同的方法列表(次序不同不要紧),那么它们就等同的,可以相互赋值。 如果A接口的方法列表时接口B的方法列表的子集,那么接口B可以赋值给接口A,但是反过来是不行的,无法通过编译。
五 接口查询
接口查询是否成功,要在运行期才能够确定。他不像接口的赋值,编译器只需要通过静态类型检查即可判断赋值是否可行。
var file1 Writer = ... if file5,ok := file1.(two.IStream);ok { ... }
这个if语句检查file1接口指向的对象实例是否实现了two.IStream
接口,如果实现了,则执行特定的代码。
在Go语言中,你可以询问它指向的对象是否是某个类型,比如,
var file1 Writer = ... if file6,ok := file1.(*File);ok { ... }
这个if语句判断file1接口指向的对象实例是否是*File类型,如果是则执行特定的代码。
slice := make([]int, 0) slice = append(slice, 1, 2, 3) var I interface{} = slice if res, ok := I.([]int);ok { fmt.Println(res) //[1 2 3] }
这个if语句判断接口I所指向的对象是否是[]int类型,如果是的话输出切片中的元素。
func Sort(array interface{}, traveser Traveser) error { if array == nil { return errors.New("nil pointer") } var length int //数组的长度 switch array.(type) { case []int: length = len(array.([]int)) case []string: length = len(array.([]string)) case []float32: length = len(array.([]float32)) default: return errors.New("error type") } if length == 0 { return errors.New("len is zero.") } traveser(array) return nil }
通过使用.(type)
方法可以利用switch来判断接口存储的类型。
요약: 인터페이스가 가리키는 객체가 특정 유형인지 질의하는 사용법은 인터페이스 질의의 특별한 경우라고 볼 수 있습니다. 인터페이스는 유형 그룹의 공개 특성을 추상화한 것이므로 인터페이스를 쿼리하는 것과 특정 유형을 쿼리하는 것의 차이는 다음 두 질문의 차이와 같습니다.
당신은 의사입니까?
그렇습니다.
You are Momomo
Yes
첫 번째 질문은 그룹을 쿼리하는 쿼리 인터페이스인 반면, 두 번째 질문은 특정 개인에게 도달한 특정 유형의 쿼리입니다.
또한, 타입 쿼리에도 리플렉션을 사용할 수 있는데, 이에 대해서는 리플렉션에서 자세히 소개하겠습니다.
더 많은 golang 관련 기술 기사를 보려면 go 언어 칼럼을 방문하세요!
위 내용은 golang의 인터페이스 인터페이스에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!