Go 其五 到底是不是面嚮對象語言 -- 封裝數據和行爲, 接口, 自定義類型

  關於Go是不是面嚮對象語言其實有很多爭論,關於給出的解釋是:Yes and no.

 

封裝數據和行爲
  結構體定義

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)
}

  


Duck Type式接口實現
  接口定義

type Programmer interface {
  WriteHelloWorld() Code
}

  

  接口實現

type GoProgrammer struct {
}

func (p *GoProgrammer) WriteHelloWorld() Code {
    return "fmt.Println(\"Hello World!\")"
}

  

 

Go接口
  與其他主要編程語言的差異

  1. 接口爲非入侵性的,實現不依賴於接口的定義
  2. 所以接口的定義可以包含在接口使用者包內


  接口變量

var prog Coder = &GoProgrammer{}
// 以上prog是接口Coder的一個變量

  

當prog被初始化之後它有兩部分

prog

type GoProgrammer stuct { //類型
類型 --> }

數據 --> &GoProgrammer{} //具體的實現

  


自定義類型

  舉例:
1. type IntConvertionFn func(n int) int
2. type MyPoint int

Go語言中接口的實現是不依賴於接口的定義的,是採用DockType的方式

 

  接口

  以上是筆者在進行Go語言面向對象方面知識所記錄的筆記,可能會有一些凌亂。如果你和我的技術棧類似,可能會對Go中的接口定義以及實現,Duck Type等部分內容感到新奇。可以看看接口這部分的代碼:

package interface_test

import (
	"testing"
)

type Programmer interface{
	WriteHelloWorld() string
}

type GoProgrammer struct{

}

func (g *GoProgrammer) WriteHelloWorld() string{
	return "fmt.Println(\"Hello World!\")"
}

/*注意這裏,其實p定義的是接口Programmer,而p = new(GoProgrammer)
  是將接口Programmer的具體實現'GoProgrammer'作爲p的實例
  這裏沒有使用傳統的【接口定義,接口實現繼承自接口定義,具體使用的地方利用接口定義通過容器或別的方式獲取接口實現】的傳統方法
  而是使用了DuckType.所以DuckType就是指,這個鳥雖然我不知道是什麼,但是看起來腳上有蹼,扁嘴,像是鴨子,那麼將當它是鴨子。(2333,這是老師的原話)
  對應上面就是接口GoProgrammer所對應的方法‘WriteHelloWorld’與Programmer中定義的方法看起來是一樣的,那麼我們就當GoProgrammer是Programmer的具體實現
*/

func TestClinet(t *testing.T){
	var p Programmer
	p = new(GoProgrammer)
	t.Log(p.WriteHelloWorld())
}

  如上述代碼,如果是以"C#"之類的語言來看,其實我們在TestClient中是定義了一個Programmer接口的變量p,而p的具體實現則是 “類” GoProgrammer 。但我們觀察 “類” GoProgrammer,其實並不像C#中那樣要繼承自Programmer接口,只是這個方法定義的相同。這就是所謂的“Duck Type”.

 

  自定義類型

  而關於自定義類型,其實是可以自定義出一些“複雜”的類型,並利用這寫類型來簡化一些功能的實現.

package customer_type

import (
	"testing"
	"fmt"
	"time"
)

type IntConv func(op int) int

//通過自定義類型,讓程序有更好的可讀性,這裏自定義了IntCov類型,這個類型是入參爲一個int,返回一個int的函數
func timeSpent(inner func(op int) int) IntConv {
	return func(n int) int {
		start := time.Now()
		ret := inner(n)
		fmt.Println("time spent:", time.Since(start).Seconds())
		return ret
	}
}

func slowFun(op int) int{
	time.Sleep(time.Second *1)
	return op
}

func TestFn(t *testing.T) {
	// 計算函數運行時間
	tsSF := timeSpent(slowFun)
	t.Log(tsSF(10))
}

  如上述代碼,我們的自定義類型IntConv其實是一個參數爲int,返回值爲int的函數。而timeSpent方法的返回值是IntConv類型,作用是計算某個函數的運行時間。如果你仔細觀察,其實會發現timeSpent不只返回值,參數其實也是IntConv類型,因此這個方法改寫成如下是完全OK的。

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
	}
}

  

  而關於行爲方法的定義,其實沒什麼好說的,只是要注意兩種方式的不同,爲了避免內存複製,儘量使用 " Employee"這種方式.

package encapsulation

import (
	"testing"
	"fmt"
	"unsafe"
)

type Employee struct {
	Id string
	Name string
	Age int
}

func TestCreateEmployeeObj(t *testing.T){
	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"
	t.Log(e)
	t.Log(e1)
	t.Log(e1.Id)
	t.Log(e2)
    //%T 代表輸出類型
	t.Logf("e is %T", e) 	//輸出 e is encapsulation.Employee
	t.Logf("e2 is %T", e2)  //輸出 e2 is *encapsulation.Employee
}

//第一種定義方式在市裏對應方法被調用時,實例的成員會進行值複製
func (e Employee) String() string {
	fmt.Printf("Address is %x\n", unsafe.Pointer(&e.Name))
	return fmt.Sprintf("ID:%s-Name:%s-Age:%d", e.Id, e.Name, e.Age)
}

//第二種通常情況下爲了避免內存拷貝我們使用第二種定義方式
func (e *Employee) String2() string {
	fmt.Printf("Address is %x\n", unsafe.Pointer(&e.Name))
	return fmt.Sprintf("ID:%s/Name:%s/Age:%d", e.Id, e.Name, e.Age)
}

func TestStructOperations(t *testing.T) {
	e := Employee{"0", "Bob", 20}
	t.Log(e.String())
	fmt.Printf("Address is %x\n", unsafe.Pointer(&e.Name))
	/*
		輸出爲:
		Address is c000064520
			TestStructOperations: encap_test.go:45: ID:0-Name:Bob-Age:20
		Address is c0000644f0
	*/

	t.Log(e.String2())
	fmt.Printf("Address is %x\n", unsafe.Pointer(&e.Name))
	/*
		輸出爲:
		Address is c0000644f0
			TestStructOperations: encap_test.go:48: ID:0/Name:Bob/Age:20
		Address is c0000644f0
	*/

	//結論,第一種方式(func (e Employee) String() string)會有更大的內存開銷,因爲是把結構的數據copy了一份,而第二種方式(func (e *Employee) String2() string)引用了相同的地址。
}

  

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章