Go 是物件導向的語言嗎?

Boyn發表於2020-03-29

該篇文章首發於boyn.top,轉載請宣告

如果某個開發人員在學習Go之前,對於Java,C#那套物件導向設計方法很熟悉的人員,在學習Go的時候,面對Go中的結構體struct,介面interface等概念,也許會產生疑問:Go語言是一門物件導向的語言嗎.這個問題的答案是:Yes and No.

我們來看看官方是怎麼說的

Yes and no. Although Go has types and methods and allows an object-oriented style of programming, there is no type hierarchy. The concept of “interface” in Go provides a different approach that we believe is easy to use and in some ways more general. There are also ways to embed types in other types to provide something analogous—but not identical—to subclassing. Moreover, methods in Go are more general than in C++ or Java: they can be defined for any sort of data, even built-in types such as plain, “unboxed” integers. They are not restricted to structs (classes).

Also, the lack of a type hierarchy makes “objects” in Go feel much more lightweight than in languages such as C++ or Java.

官網中對於Go是否為一門物件導向的語言這個問題的表述為:

是,也不是.雖然Go語言可以通過定義型別和方法來實現物件導向的設計風格,但是Go是實際上並沒有繼承這一說法.在Go語言中,interface(介面)這個概念以另外一種角度展現了一種更加易用與通用的設計方法.在Go中,我們可以通過組合,也就是將某個型別放入另外的一個型別中來實現類似繼承,讓該型別提供有共性但不相同的功能.相比起C++和Java,Go提供了更加通用的定義函式的方法,我們可以指定函式的接受物件(receiver),它可以是任意的型別,包括內建型別,在這裡沒有任何的限制.

同樣的,沒有了型別繼承,使得Go語言在物件導向程式設計的方面會顯得更加輕量化

對於上面的說法,我們可以知道,Go語言實際上可以作為一種物件導向的語言來使用,但是它並不完全像Java一樣是完全的面嚮物件語言.它缺失了一些物件導向定義中的特性,而又用其他的特性來填補這些缺失.是否更好呢?我個人認為Go語言的設計哲學比起Java來會更加的適合於大型專案的開發,我們往往可以不用在意一些類的繼承類圖,專注於他們自身的特性,而開發者自身在開發時,也可以通過組合等方式來複用他們的邏輯,這對於雙方都是有益的.

但是,我們如果死抱教條不放而死板地使用物件導向特性來開發的話,Go語言難免就會變成一門比較複雜的語言,從而部分地失去了其簡潔的特性

正如我們之前所說,Go語言有其自身的物件導向特性,與C++,Java的物件導向是十分不同的.在這一節中,我們就來看看Go語言是如何使用Go完成物件導向特性的.

首先,我們要了解,對於Go來說,這幾個概念對於物件導向是必不可少的:Method,Structure,Interface,Receiver,建議不瞭解Go基本語法的同學先去看一下Go的基本語法,這篇文章並不是一篇Go語言的入門文章.

我們知道,物件導向的三大特性是封裝,繼承和多型.接下來,我們就一步步看看怎麼用Go來完成這些特性

Go語言絕對不允許迴圈引入,即 package A import B & package B import A

封裝

什麼是封裝呢?簡單來說,我們定義了一個結構體,並定義了它的一些方法後,需要進行一定的抽象與邏輯的抽取,可能會有多個方法會執行一段相同的邏輯,我們就可以把這段邏輯抽出來作為一個函式,但是它並不能直接被外界所感知.在Go語言中,封裝是基於包(package)的,而不是基於結構體,在同一個包下,我們可以訪問任何定義在這個包中的變數與方法,而在外部的包中,只能訪問這個包被匯出的結構體與方法.在Go中,匯出是一個很簡單的事情,我們將type的命名首字母大寫,則可以讓其匯出,而如果將其設為小寫,則是不匯出.

我們來看看實際的程式碼:

package model

import "fmt"

//Author: Boyn
//Date: 2020/3/29

type User struct {
    Username string
    Password string
    Age      int
}

func NewUser(name, password string, age int) *User {
    return &User{
        Username: name,
        Password: password,
        Age:      age,
    }
}

func (u *User) SetUsername(username string) {
    u.logForChange("Username", username)
    u.Username = username
}

func (u *User) SetAge(i int) {
    u.logForChange("Age", i)
    u.Age = i
}

func (u *User) SetPassword(password string) {
    u.logForChange("Password", password)
    u.Password = password
}

func (u *User) logForChange(field string, value interface{}) {
    fmt.Printf("%s changed to %v", field, value)
}

在這裡,我們定義了一個User結構體與其構造方法,並且設定了若干setter方法,而在每次set值的時候,都會列印一條訊息來說明更改的域與內容.在這裡,列印日誌這個邏輯是可以抽象出來的,但是不能讓使用這個包的人直接呼叫,所以我們可以把他設為不匯出,也就是首字母小寫

繼承

在Go語言中,並沒有顯式的繼承與顯式的介面實現(介面實現其實也算是一種繼承),Go對於繼承,是通過組合來實現的,我們如果在父結構體中引入了匿名的其他結構體,那麼它的所有成員變數和方法都可以直接通過父結構體來進行訪問,而介面的實現也是隱式的,我們只需要實現某個介面的所有方法,就可以認為是實現了這個介面(如果兩個介面都有同樣的方法,那麼就可以認為是實現了這兩個介面,這為多型帶來了便利,在下一節中將會詳細說明).

在這一小節中,我們同樣來看看實際的程式碼

package model

import "fmt"

//Author: Boyn
//Date: 2020/3/29

type Post struct {
    title string
    content string
    User
}

func (p *Post) details(){
    fmt.Printf("Title: %s\nContent: %s\n User: %s\n",p.title,p.content,p.Username)
}

在這裡,我們將上面一小節的User進行重用,他們都在一個包下.

我們看到在Post中,引入了一個匿名成員User,現在,我們可以直接通過Post引用User裡面的內容了,如果為了方便辨認,同樣可以使用p.User.Username這樣的方式引用User的內容

多型

在Go語言中,多型是依靠介面來實現的,我們知道,一個介面是隱式地被實現的,當我們實現了這個介面的所有方法,就認為我們實現了這個介面.

我們需要擴充上面定義的程式,使得文章擁有傳送文章的功能.即給文章定義一個post方法,可以通過Poster將文章投遞到對應的部落格系統中(此處以Hexo和Hugo為例)

package model

import "fmt"

//Author: Boyn
//Date: 2020/3/29

type Poster interface {
    postArticle(p Post) error
}

type HexoPoster struct {
    url string
}

func (h *HexoPoster) postArticle(p Post) error {
    fmt.Println("Posted Article to Hexo on "+h.url)
    return nil
}

type HugoPoster struct {
    url string
}

func (h *HugoPoster) postArticle(p Post) error {
    fmt.Println("Posted Article to Hugo on "+h.url)
    return nil
}

type Post struct {
    title string
    content string
    User
    Poster
}

func (p *Post) post(){
    _ = p.Poster.postArticle(*p)
}

func (p *Post) details(){
    fmt.Printf("Title: %s\nContent: %s\n User: %s\n",p.title,p.content,p.Username)
}

在這個程式中,Post多了一個成員變數Poster,我們在初始化的時候需要定義它,可以是HexoPoster或者HugoPoster.這樣我們在呼叫post()方法的時候,不需要指定特定的部落格系統,而是可以動態指定,使得程式的可擴充性更強.

image-20200329142137618

  1. https://golang.org/doc/faq#Is_Go_an_object... Go語言的官方問答
  2. https://www.ardanlabs.com/blog/2014/05/met... 通過實驗來說明方法,介面與embedded-type的關係
  3. https://golangbot.com/learn-golang-series/ 26-28章展示了Go語言的物件導向特性
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章