1 封裝
Java 中封裝是基於類(Class),Golang 中封裝是基於結構體(struct)
Golang 的開發中經常直接將成員變數設定為大寫使用,當然這樣使用並不符合物件導向封裝的思想。
Golang 沒有建構函式,但有一些約定俗成的方式:
- 提供 NewStruct(s Struct) *Struct 這樣的函式
- 提供 (s *Struct) New() 這樣的方法
- 也可以直接用傳統的 new(struct) 或 Struct{} 來初始化,隨後用 Set 方法對成員變數賦值
type People struct {
name string
age int
}
func (p *People) SetName(name string) {
p.name = name
}
func (p *People) GetName() string {
return p.name
}
func (p *People) SetAge(age int) {
p.age = age
}
func (p *People) GetAge() int {
return p.age
}
func main() {
peo := new(People)
peo.SetName("張三")
peo.SetAge(13)
fmt.Println(peo.GetName(), peo.GetAge())
// 張三 13
}
2 繼承 or 組合
Golang 不支援繼承,支援組合,算是“組合優於繼承”思想的體現。
雖然不支援繼承,但 Golang 的匿名組合組合可以實現物件導向繼承的特性。
2.1 非匿名組合和匿名組合
組合分為非匿名組合
和匿名組合
非匿名組合不能直接使用內嵌結構體的方法,需要透過內嵌結構體的變數名,間接呼叫內嵌結構體的方法。
匿名組合可以直接使用內嵌結構體的方法,如果有多個內嵌結構體,可以直接使用所有內嵌結構體的方法。
非匿名組合例項:
type People struct {
name string
age int
}
func (p *People) SetName(name string) {
p.name = name
}
func (p *People) GetName() string {
return p.name
}
func (p *People) SetAge(age int) {
p.age = age
}
func (p *People) GetAge() int {
return p.age
}
type Student struct {
people People
grade string
}
func (s *Student) SetGrade(grade string) {
s.grade = grade
}
func (s *Student) GetGrade() string {
return s.grade
}
func main() {
stu := Student{}
stu.people.SetName("張三")
stu.people.SetAge(13)
stu.SetGrade("七年級")
fmt.Println(stu.people.GetName(), stu.people.GetAge(), stu.GetGrade())
// 張三 13 七年級
}
非匿名組合主要體現在 Student
結構體中對 People
的組合需要明確命名。
匿名組合例項:
type People struct {
name string
age int
}
func (p *People) SetName(name string) {
p.name = name
}
func (p *People) GetName() string {
return p.name
}
func (p *People) SetAge(age int) {
p.age = age
}
func (p *People) GetAge() int {
return p.age
}
type Student struct {
People
grade string
}
func (s *Student) SetGrade(grade string) {
s.grade = grade
}
func (s *Student) GetGrade() string {
return s.grade
}
func (s *Student) GetName() string {
return fmt.Sprintf("student-%s",s.People.GetName())
}
func main() {
stu := Student{}
stu.SetName("張三")
stu.SetAge(13)
stu.SetGrade("七年級")
fmt.Println(stu.GetName(), stu.GetAge(), stu.GetGrade())
// student-張三 13 七年級
}
從上面例項可以看出,匿名組合中:
Student
中的People
沒有顯式命名Student
可以直接使用People
的方法- 注意
Student
的GetName
方法,與People
中的GetName
方法重複,可以看做是面相物件程式設計中的重寫(Overriding)
Student
的GetName
方法中使用s.People.GetName()
呼叫People
中的GetName
方法,這是對匿名組合的顯式呼叫,類似 Java 中的 super 用法
可以看出,匿名組合的使用感官上類似物件導向程式設計的繼承,可以說是一種『偽繼承』的實現,但匿名組合並不是繼承!
2.2 組合的使用方式
2.2.1 結構體中內嵌結構體
上面例項所用的就是結構體中內嵌結構體,不再多說。
2.2.2 結構體中內嵌介面
內嵌介面如下面例子:
type Member interface {
SayHello() // 問候語
DoWork() // 開始工作
SitDown() // 坐下
}
type Normal struct{}
func (n *Normal) SayHello() {
fmt.Println("normal:", "大家好!")
}
func (n *Normal) DoWork() {
fmt.Println("normal:", "記筆記")
}
func (n *Normal) SitDown() {
fmt.Println("normal:", "坐下")
}
type People struct {
name string
age int
}
func (p *People) SetName(name string) {
p.name = name
}
func (p *People) GetName() string {
return p.name
}
func (p *People) SetAge(age int) {
p.age = age
}
func (p *People) GetAge() int {
return p.age
}
type Student struct {
Member
People
grade string
}
func (s *Student) SetGrade(grade string) {
s.grade = grade
}
func (s *Student) GetGrade() string {
return s.grade
}
func (s *Student) SayHello() {
fmt.Println("student:", s.name, "說: 老師好!")
}
type Teacher struct {
Member
People
subject string
}
func (t *Teacher) SetSubject(subject string) {
t.subject = subject
}
func (t *Teacher) GetSubject() string {
return t.subject
}
func (t *Teacher) SayHello() {
fmt.Println("teacher", t.name, "說: 同學們好!")
}
func (t *Teacher) DoWork() {
fmt.Println("teacher", t.name, "講課!")
}
func main() {
stu := &Student{Member: &Normal{}}
stu.SetName("張三")
stu.SetAge(13)
stu.SetGrade("七年級")
tea := &Teacher{Member: &Normal{}}
tea.SetName("李四")
tea.SetAge(31)
tea.SetSubject("語文")
var member Member
member = stu
member.SayHello()
member.SitDown()
member.DoWork()
// student: 張三 說: 老師好!
// normal: 坐下
// normal: 記筆記
member = tea
member.SayHello()
member.SitDown()
member.DoWork()
// teacher 李四 說: 同學們好!
// normal: 坐下
// teacher 李四 講課!
}
從上面例子可以看出:
Teacher
和Student
結構體都沒有完全實現Member
的方法Normal
結構體實現了Member
方法Teacher
和Student
初始化時注入了Normal
結構Teacher
和Student
可以作為Member
型別的介面使用,並預設使用Normal
的實現。除非Teacher
和Student
有自己的實現。
Teacher
和 Student
並非沒有實現 Member
介面。編譯器自動為型別 *Teacher
和 *Student
實現了 Member
中定義的方法,類似:
func (t *Teacher) SitDown() {
t.Member.SitDown()
}
所以即使在初始化時沒有指定 Normal
,Teacher
和 Student
也可以賦值給 Member
型別的變數。但呼叫未實現的 Member
的方法時會報 panic
官方實踐
可以參考 sort.Reverse
方法,reverse 結構體內嵌了 Interface 介面,並只實現了 Less 方法
type IntSlice []int
func (x IntSlice) Len() int { return len(x) }
func (x IntSlice) Less(i, j int) bool { return x[i] < x[j] }
func (x IntSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
type Interface interface {
// 長度
Len() int
// 對比兩個數 i,j,返回結果作為排序的依據
Less(i, j int) bool
// 交換兩個數 i,j
Swap(i, j int)
}
type reverse struct {
Interface
}
func (r reverse) Less(i, j int) bool {
return r.Interface.Less(j, i)
}
func Reverse(data Interface) Interface {
return &reverse{data}
}
使用時:
lst := []int{4, 5, 2, 8, 1, 9, 3}
sort.Sort(sort.Reverse(sort.IntSlice(lst)))
fmt.Println(lst)
// 列印:[9 8 5 4 3 2 1]
可以看出,Reverse 就是用內嵌介面的方式,接收一個 Interface 介面,將 Less 方法的兩個引數反轉了。
2.2.3 介面中內嵌介面
是針對方法的組合。如 Golang 的 ReadWriter 介面就是 Reader 和 Writer 介面的組合。
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
// ReadWriter is the interface that groups the basic Read and Write methods.
type ReadWriter interface {
Reader
Writer
}
3 多型
多型的定義比較寬鬆:指一個行為具有多種不同的表現形式。
本質上多型分兩種:編譯時多型(靜態)和執行時多型(動態)
- 編譯時多型在編譯期間,多型就已經確定。過載是編譯時多型的一個例子。
- 執行時多型在編譯時不確定呼叫哪個具體方法,一直延遲到執行時才能確定。
通常情況下,我們討論的多型都是執行時多型。Golang 介面就是基於動態繫結實現的多型。
由於 Golang 結構體是『組合』而非『繼承』,不能相互轉換,所以只有基於介面的多型。
3.1 向上轉型
向上轉型是實現多型的必要條件。即用父類的引用指向一個子類物件,透過父類引用呼叫方法時會呼叫子類的方法。透過父類引用無法呼叫子類的特有方法,需要『向下轉型』。
type Member interface {
SayHello() // 問候語
DoWork() // 開始工作
SitDown() // 坐下
}
type People struct {
name string
age int
}
func (p *People) SetName(name string) {
p.name = name
}
func (p *People) GetName() string {
return p.name
}
func (p *People) SetAge(age int) {
p.age = age
}
func (p *People) GetAge() int {
return p.age
}
type Student struct {
People
grade string
}
func (s *Student) SetGrade(grade string) {
s.grade = grade
}
func (s *Student) GetGrade() string {
return s.grade
}
func (s *Student) SayHello() {
fmt.Println("student:", s.name, "說: 老師好!")
}
func (s *Student) DoWork() {
fmt.Println("student:", s.name, "記筆記")
}
func (s *Student) SitDown() {
fmt.Println("student:", s.name, "坐下")
}
type Teacher struct {
People
subject string
}
func (t *Teacher) SetSubject(subject string) {
t.subject = subject
}
func (t *Teacher) GetSubject() string {
return t.subject
}
func (t *Teacher) SayHello() {
fmt.Println("teacher", t.name, "說: 同學們好!")
}
func (t *Teacher) DoWork() {
fmt.Println("teacher", t.name, "講課!")
}
func (t *Teacher) SitDown() {
fmt.Println("teacher:", t.name, "站著講課,不能坐下!")
}
func main() {
stu := &Student{}
stu.SetName("張三")
stu.SetAge(13)
stu.SetGrade("七年級")
tea := &Teacher{}
tea.SetName("李四")
tea.SetAge(31)
tea.SetSubject("語文")
var member Member
member = stu
member.SayHello()
member.SitDown()
member.DoWork()
// student: 張三 說: 老師好!
// student: 張三 坐下
// student: 張三 記筆記
member = tea
member.SayHello()
member.SitDown()
member.DoWork()
// teacher 李四 說: 同學們好!
// teacher: 李四 站著講課,不能坐下!
// teacher 李四 講課!
}
這裡是基於介面實現的『向下轉型』,沒有父類、子類之分。而在 Java 中,當子類沒有重寫父類方法時,父類的引用會呼叫到父類的方法裡。其實也有類似的實現,在上面已經有了,即2.2.2 結構體中內嵌介面
3.2 向下轉型
上面提到,用父類的引用指向一個子類物件,透過父類引用無法呼叫子類的特有方法。但在某些情況下需要呼叫子類的特有方法,例如子類有一些特殊邏輯需要處理,這時就需要『向下轉型』還原出子類的引用。
在 Java 裡,通常用 User user = (User) people;
來向下轉型。
Golang 向下轉型透過型別斷言。基本用法是t,ok := intefaceValue.(T)
。
一個例子:
type Member interface {
SayHello() // 問候語
}
type People struct {
name string
age int
}
func (p *People) SetName(name string) {
p.name = name
}
func (p *People) GetName() string {
return p.name
}
func (p *People) SetAge(age int) {
p.age = age
}
func (p *People) GetAge() int {
return p.age
}
type Student struct {
People
grade string
}
func (s *Student) SetGrade(grade string) {
s.grade = grade
}
func (s *Student) GetGrade() string {
return s.grade
}
func (s *Student) SayHello() {
fmt.Println("student:", s.name, "說: 老師好!")
}
func main() {
stu := &Student{}
stu.SetName("張三")
stu.SetAge(13)
stu.SetGrade("七年級")
var member Member = stu
member.SayHello()
// student: 張三 說: 老師好!
if student, ok := member.(*Student); ok {
student.SetName("王五")
student.SayHello()
// student: 王五 說: 老師好!
}
}
- 使用
Member
型別的引用,指向一個Student
物件 - 想對
Student
物件設定一個新名稱,使用型別斷言向下轉型,可以呼叫Student
的SetName
方法(嚴格的說,是People
的SetName
方法)
除了上面場景,還有一種場景需要型別斷言:當有一個函式,其引數是 interface{} 型別時:
func SayHello(inter interface{}) {
if member, ok := inter.(Member); ok {
member.SayHello()
}
}
func main() {
stu := &Student{}
stu.SetName("張三")
stu.SetAge(13)
stu.SetGrade("七年級")
SayHello(stu)
// student: 張三 說: 老師好!
}
補充:除了型別斷言,Golang 還有一種 type-switch 的方式可以用於對 interface 的型別探測,從而進行不同邏輯的處理。