標籤:

go語言介面的優勢?

面向對象的語言都有介面的概念(interface),介面用來做編程規範,現在go語言也實現了介面,還是以一種新的方式,現在有點疑問,介面的實際意義是什麼?在編程中如何體現? 麻煩各位大神指點


go的介面更像是C++那種帶concept的模板的運行時模式。好處在於寫庫不需要實現成侵入式(大部分情況下指的是要求參數的類必須繼承自某個介面),壞處是代碼不收你直覺上的控制,因為你要麼特化出函數使得代碼膨脹,要麼你要生成adaptor使得性能下降。

go的interface的好處自然跟C++的concept的好處是差不多的,就是對參數的需求是明確的,不滿足就編譯不通過,錯誤信息也好看,不會跟JS一樣寫成狗屎。


審題後覺得題主應該是倆問題

第一,介面的意義是什麼?

我覺得大概就是抽象一個層,讓使用者關注」這個東西怎麼用「,而不用關注具體是什麼吧,統一的介面也可以讓你的程序拆裝更方便(比如你用個Parser介面來解析配置文件,下面可以掛XmlParser,也可以掛TomlParser,而使用者的代碼不需要跟著變)

第二,go的介面實現方式和其他的不同

傳統的像java的侵入式介面,類需要顯式實現介面,所以可以看做是先有介面再有實現,這會有一個問題,就是實現某個功能時候,可能會被當時的介面需求約束住,反過來如果沒有介面方面的需求,則一個類庫很難確定自己應該提供什麼介面出去,因為外面的需求是未知的,你也不可能提供所有可能的介面出去,因為數量是2^N

而go的非侵入式介面就將介面的定義和責任轉移到使用者這頭,實現功能的人不需要關心別人想怎麼用,他只要告訴別人自己的東西可以怎麼用就行了,使用者如果想簡單地用這個功能,就直接用(比如直接用XmlParser),如果想抽象,就自己去抽象(自己定義Parser介面然後把XmlParser掛進來)

為啥說責任也過來了呢,因為介面並不僅僅是一堆形式上的方法聲明,而是有其業務含義的,但是編譯器沒法給你去檢查其業務含義,業務是人自己定義的,比如這個例子:

type Collection interface {

size() int //返回集合的元素數量

}

假設這是一個集合介面,使用者的原意是,它下面可以掛Vector、LinkedList、TreeMap、HashMap等各種數據結構,而size方法返回這些結構中元素的數量,然後這個人從各種渠道下載了這些數據結構的實現,結果發現了這麼個東東:

func (v *Vector) len() int //返回元素數量

func (v *Vector) size() int //返回Vector當前reserved的大小(也就是capacity)

這個實現者可能是python的粉絲,習慣用len來表示數量,然後還弄了個size表示其他含義,但是編譯器只看介面是否適配,如果簡單地把這個Vector掛給Collection,顯然是錯誤的設計

這時候可能就需要使用者自己再包一層了:

type MyVector struct {

Vector

}

func (v *MyVector) size() int {

return v.Vector.len()

}

go的介面還有一些其他可能的問題,比如從設計理念上說,」能這樣用的都適配這個介面「,但實現上卻要求」必須嚴格實現這個介面定義的所有方法「,」能這樣用「和」完全一樣「有時候並不等價,因為前者允許安全的協變和抗變,不過go的實現不支持

還有一些不是介面的鍋但跟介面有關的,比如不支持泛型,用介面來代替,但是介面的表達能力不如泛型代碼等


想到的第一個例子是 encoding/json 的自定義編解碼。

對於一個 struct 來說,encoding/json 允許通過 tag 來定義每個 exported field 的 JSON 輸出。

但如果你的類型所需要的 JSON 格式和 struct 本身結構不同,或者要包含 unexported 的內容,怎麼辦?

如果 encoding/json 要求只能 Marshal 某個特定類型,那麼你是沒辦法讓自己定義的類型被 marshal 接受的。

但 encoding/json 定義了 Marshaler interface,約定如果你的類型實現了這個 interface (也就是提供了 MarshalJSON 方法,Marshal 就會調用你自己定義的 MarshalJSON 方法而不用去分析你的 field 並強制輸出格式。

interface 就是用來定義調用者和被調用代碼之間建立的這個約定用的。

type nonExported struct {
a, b string
}

func (x nonExported) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`{"ab":[%q, %q]}`, x.a, x.b)), nil
}


就算 go 不提供介面這個語言機制,用 struct 也能實現類似的功能:

package main

import "fmt"

func main() {

// 介面定義
type Interface struct {
Foo func() string
Bar func() string
}

// 參數為介面的函數
fn := func(i Interface) {
// 調用介面方法
fmt.Printf("%s
", i.Foo())
fmt.Printf("%s
", i.Bar())
}

// 以匿名函數實現介面
fn(Interface{
Foo: func() string {
return "FOO"
},
Bar: func() string {
return "BAR"
},
})

// 以類型方法實現介面
foo := Foo{}
bar := Bar{}
fn(Interface{
Foo: foo.Foo, // 不需要同一個類型
Bar: bar.Bar,
})

// 混合匿名函數和類型方法
fn(Interface{
Foo: foo.Foo,
Bar: func() string {
return "Bar"
},
})

// 另一個以介面為參數的函數,提供了介面方法的默認實現
fn2 := func(i Interface) {
if i.Foo == nil {
i.Foo = func() string {
return "default FOO"
}
}
if i.Bar == nil {
i.Bar = func() string {
return "default Bar"
}
}
fn(i)
}

fn2(Interface{
// 不傳入 Foo,使用默認實現
Bar: bar.Bar,
})

}

type Foo struct{}

func (f Foo) Foo() string {
return "FOO"
}

type Bar struct{}

func (b Bar) Bar() string {
return "BAR"
}

go 里的函數類型是統一的,一個 func() string 類型的欄位,傳入任意的普通函數、匿名函數、類型方法都可以,只要簽名一致。類似 C++ 的 std::function 或者 C# 的 delegate,但並不需要額外的概念。


像鴨子

隱式實現,非侵入

比如你如果要寫一個 TCP 庫,想要在標準庫的基礎上加上回調函數

你就可以定義一個介面

type Actor interface {
OnConnect() error
OnMessage(data []byte) error
OnError(err error)
OnClose()
GetMsgChan() &<-chan []byte Done() &<-chan struct{} }

然後定義一個 Controller

type TCPCtrl struct {
Actor
}

func (t *TCPCtrl) Scan() {
defer t.OnClose()
err := t.OnConnect()
if err != nil {
t.OnError(err)
}

Circle:
for {
select {
case &<-t.Done(): break Circle case msg := &<- t.GetMsgChan(): err := t.OnMessage(msg) if err != nil { t.OnError(err) } }

然後如果你要使用這個庫,可以隨便定義自己的ActorType,只需要保證你的 Actor 有四個回調函數,一個 MsgChannel,和一個Done(使用Context),然後就可

ctrl := TCPCtrl{NewActorType()}
go ctrl.Scan()

當然我還沒有說隱式實現介面有什麼好處

可以跟一些「顯式實現」的介面,比如 Rust 的 trait 做對比

如果 Rust 要實現上面的效果大概要這樣

use std::io::*;
use std::error::Error;
use std::sync::mpsc;

#[derive(Debug)]
pub enum Message {
Ok(Vec&),
Terminate,
}

pub trait Actor {
fn on_connect(mut self) -&> Result&<()&>;
fn on_message(mut self, [u8]) -&> Result&<()&>;
fn on_error(mut self, Box&);
fn on_close(mut self);
fn try_recv(mut self) -&> Message;
}

pub struct TCPCtrl {
actor: Box&,
}

impl TCPCtrl {
pub fn scan(mut self) -&> Result&<()&> {
if let Err(err) = self.actor.on_connect() {
self.actor.on_error(Box::new(err));
}
loop {
match self.actor.try_recv() {
Message::Ok(msg) =&> {
if let Err(err) = self.actor.on_message(msg[..]) {
self.actor.on_error(Box::new(err));
}
}
Message::Terminate =&> {
self.actor.on_close();
return Ok(());
}
}
}
}
}

然後如果你需要調用這個包,定義了自己的 ActorType

之後需要

impl Actor for ActorType {
fn on_connect(mut self) -&> Result&<()&> {
unimplemented!()
}
...
}

顯示地實現了所有的方法之後,再有

let mut ctrl = TCPCtrl { actor: Box& };
let handler = thread::spawn(move || ctrl.scan());

好的,那這兩種實現有什麼區別呢?

Go 的 interface 寫起來更自由,無需顯式實現,只要實現了與 interface 所包含的所有函數簽名相同的方法即可。

Rust 需要顯示實現 trait ,但本人認為這是一種更好的實現方式,把同一個 trait 所包含的函數實現在同一個 block ,方便統一更改。

另外,Go 的隱式實現有一個問題我還沒找到很好的解決方案

比如

type A interface {
...
Func() error
}

type B interface {
...
Func()
}

type C int;

func funcA(A) {}
func funcB(B) {}

func (c *C) Func ???

即兩個 interface 發生了函數簽名衝突,但是我 funcA 需要 A ,funcB 需要 B ,這時候 C 的 Func 函數要怎麼定義呢?

或者,即使 A 和 B 的 Func 函數簽名相同,但需要有不同的功能,C 的 Func 又該如何定義呢?

我想到的一個不錯的解決方案是

type CA struct {
C
}

type CB struct {
C
}

然後分別實現 A 和 B 兩個 interface

這樣還是不夠優雅,如果有更好的解決方法,希望大家能多多指教

但是這在 Rust 裡面就完全沒問題了

use std::io::*;

fn main() {
let b = Baz;
b.f();
&::f(b, "hello");
if let Err(err) = &::f(b) {
panic!(err);
}
}

trait Foo {
fn f(self, string: str);
}
trait Bar {
fn f(self) -&> Result&<()&> ;
}
struct Baz;
impl Foo for Baz {
fn f(self, string: str) {
println!("Baz"s impl of Foo {}", string);
}
}
impl Bar for Baz {
fn f(self) -&> Result&<()&> {
println!("Baz"s impl of Bar");
Ok(())
}
}
impl Baz {
fn f(self) {
println!("Baz"s impl");
}
}

另外,Rust 的 trait 還可以有默認實現,所以開頭的 Actor 可以這樣

pub trait Actor: Sized {
fn on_connect(mut self) -&> Result&<()&>;
fn on_message(mut self, [u8]) -&> Result&<()&>;
fn on_error(mut self, Box&);
fn on_close(mut self);
fn try_recv(mut self) -&> Message;
fn scan(mut self) -&> Result&<()&> {
if let Err(err) = self.on_connect() {
self.on_error(Box::new(err));
}
loop {
match self.try_recv() {
Message::Ok(msg) =&> {
if let Err(err) = self.on_message(msg[..]) {
self.on_error(Box::new(err));
}
}
Message::Terminate =&> {
self.on_close();
return Ok(());
}
}
}
}
}

然後可以直接

impl Actor for ActorType {
fn on_connect(mut self) -&> Result&<()&> {
unimplemented!()
}
...
}

但不要覆蓋 scan

然後就可以直接

let mut actor = ActorType::new();
let handler = thread::spawn(move || actor.scan());

所以,綜合來說,我認為 Go 的 interface 設計是沒有 Rust 的 trait 巧妙的,歡迎評論區討論。


go語言的interface和embedded type特性結合起來可以達到事半功倍的效果。

例如在go語言中定義介面

Type Foo interface {

Foo1()

Foo2()

Foo3()

}

在實現該介面的時候可以將介面中的三個函數分開到三個子系統中分別實現,然後再組合起來,這樣就實現了整個Foo介面。

Type A struct { foo1}

Type B struct { foo2}

Type C struct { foo3}

Type D struct { A B C}

D實現了Foo介面。

如果使用C#語言,那麼只能這樣寫

Interface Foo{

Foo1()

Foo2()

Foo3()

}

Class D : Foo{}

如果在C#的實現中foo1,foo2,foo3也是由三個不同的模塊實現的,那麼class D中就會出現下面這種多餘的代碼:

Class D:Foo

foo1(){ m_A.foo1()}

.......

在實現一套複雜系統並採用微服務架構時,服務的功能被拆分得很細,根據業務的需要可能經常需要將原有服務打散或者將打散的服務合併的情況,go語言的介面和其embedded type特性讓這一過程變得簡單。


介面是Go的精髓,即使是在標準庫也大量使用介面。

一個典型的例子是,Go里常用的 error 其實就是一個interface

type error interface {
Error() string
}


Go的interface設計還有點Java超類的意思,所有類型都實現空介面..

所以還可以這樣玩:

type Object interface{}

objects := make(map[int]Object)
objects[0] = "sb"
objects[1] = 2

for key, object := range objects {
fmt.Println("key: ", key, " value: ", object)
}


推薦閱讀:

LintCode,hihoCoder,LeetCode有什麼區別?
是什麼原因導致國內計算機教育不教 vim/emacs 這類編輯器的使用?
有沒有人怎麼都學不會演算法?
有沒有什麼操作方便的非圖形操作系統?
公司沒有大牛好不好?

TAG:編程 | Go語言 |