1. 程式人生 > >Java程式設計師學習Go指南(二)

Java程式設計師學習Go指南(二)

摘抄:https://www.luozhiyun.com/archives/211

Go中的結構體

構建結構體

如下:

type AnimalCategory struct {
    kingdom string // 界。
    phylum  string // 門。
    class   string // 綱。
    order   string // 目。
    family  string // 科。
    genus   string // 屬。
    species string // 種。
}

func (ac AnimalCategory) String() string {
    return fmt.Sprintf("%s%s%s%s%s%s%s",
        ac.kingdom, ac.phylum, ac.class, ac.order,
        ac.family, ac.genus, ac.species)
}

我們在Go中一般構建一個結構體由上面程式碼塊所示。AnimalCategory結構體中有7個string型別的欄位,下邊有個名叫String的方法,這個方法其實就是java類中的toString方法。其實這個結構體就是java中的類,結構體中有屬性,有方法。

category := AnimalCategory{species: "cat"} 
fmt.Printf("The animal category: %s\n", category)

我們在上面的程式碼塊中初始化了一個AnimalCategory型別的值,並把它賦給了變數category,通過呼叫fmt.Printf方法呼叫了category例項內的String方法,⽽⽆需 顯式地調⽤它的String⽅法。

在結構體中宣告一個嵌入欄位

因為在Go中是沒有繼承一說,所以使用了嵌入欄位的方式來實現型別之間的組合,實現了方法的重用。

這裡繼續用到上面的結構體AnimalCategory

type Animal struct {
    scientificName string // 學名。
    AnimalCategory        // 動物基本分類。
}

欄位宣告AnimalCategory代表了Animal型別的⼀個嵌⼊欄位。Go語⾔規範規定,如果⼀個欄位 的宣告中只有欄位的型別名⽽沒有欄位的名稱,那麼它就是⼀個嵌⼊欄位,也可以被稱為匿名欄位。嵌⼊欄位的型別既是型別也是名稱。

如果要像java中引用欄位裡面的屬性,那麼可以這麼寫:

func (a Animal) String() string {
    return a.AnimalCategory.String()
}

這裡還是和java是一樣的,但是接下來要講的卻和java有很大區別

由於我們在AnimalCategory中寫了一個String的方法,如果我們沒有給Animal寫String的方法,那麼我們直接列印會得到什麼結果?

    category := AnimalCategory{species: "cat"}

    animal := Animal{
        scientificName: "American Shorthair",
        AnimalCategory: category,
    }
    fmt.Printf("The animal: %s\n", animal)

在這裡fmt.Printf函式相當於呼叫animal的String⽅法。在java中只有父類才會做到方法的覆蓋,但是在Go中,嵌⼊欄位的⽅法集合會被⽆條件地合併進被嵌⼊型別的⽅法集合中。

如果為Animal型別編寫⼀個String⽅法,那麼會將嵌⼊欄位AnimalCategory的String⽅法被“遮蔽”了,從而呼叫Animal的String方法。

只 要名稱相同,⽆論這兩個⽅法的簽名是否⼀致,被嵌⼊型別的⽅法都會“遮蔽”掉嵌⼊欄位的同名⽅法。也就是說不管返回值型別或者方法引數如何,只要名稱相同就會遮蔽掉嵌⼊欄位的同名⽅法。

指標方法

上面我們的例子其實都是值方法,下面我們舉一個指標方法的例子:

func main() {
    cat := New("little pig", "American Shorthair", "cat")
    cat.SetName("monster") // (&cat).SetName("monster")
    fmt.Printf("The cat: %s\n", cat)

    cat.SetNameOfCopy("little pig")
    fmt.Printf("The cat: %s\n", cat)

}
type Cat struct {
    name           string // 名字。
    scientificName string // 學名。
    category       string // 動物學基本分類。
}
//構造一個cat例項
func New(name, scientificName, category string) Cat {
    return Cat{
        name:           name,
        scientificName: scientificName,
        category:       category,
    }
}
//傳指標設定cat名字
func (cat *Cat) SetName(name string) {
    cat.name = name
}
//傳入值
func (cat Cat) SetNameOfCopy(name string) {
    cat.name = name
}
func (cat Cat) String() string {
    return fmt.Sprintf("%s (category: %s, name: %q)",
        cat.scientificName, cat.category, cat.name)
}

在這個例子中,我們為Cat設定了兩個方法,SetName是傳指標的方法,SetNameOfCopy是傳值的方法。

⽅法SetName的接收者型別是Cat。Cat左邊再加個代表的就是Cat型別的指標型別。

我們通過執行上面的例子可以得出,值⽅法的接收者是該⽅法所屬的那個型別值的⼀個副本。⽽指標⽅法的接收者,是該⽅法所屬的那個基本型別值的指標值的⼀個副本。我們在這樣的⽅法內對該副本指向的值進⾏ 修改,卻⼀定會體現在原值上。

介面型別

宣告

type Pet interface {
    SetName(name string)
    Name() string
    Category() string
}

當資料型別中的方法實現了介面中的所有方法,那麼該資料型別就是該介面的實現型別,如下:

type Pet interface {
    Name() string
    Category() string
    SetName(name string)
}

type Dog struct {
    name string // 名字。
}

func (dog *Dog) SetName(name string) {
    dog.name = name
}

func (dog Dog) Name() string {
    return dog.name
}

func (dog Dog) Category() string {
    return "dog"
}

在這裡Dog型別實現了Pet介面。

介面變數賦值

介面變數賦值也涉及了值傳遞和指標傳遞的概念。如下:

    // 示例1
    dog := Dog{"little pig"}
    fmt.Printf("The dog's name is %q.\n", dog.Name())
    var pet Pet = dog
    dog.SetName("monster")
    fmt.Printf("The dog's name is %q.\n", dog.Name())
    fmt.Printf("This pet is a %s, the name is %q.\n",
        pet.Category(), pet.Name())
    fmt.Println()

    // 示例2。
    dog = Dog{"little pig"}
    fmt.Printf("The dog's name is %q.\n", dog.Name())
    pet = &dog
    dog.SetName("monster")
    fmt.Printf("The dog's name is %q.\n", dog.Name())
    fmt.Printf("This pet is a %s, the name is %q.\n",
        pet.Category(), pet.Name())

返回

The dog's name is "little pig".
The dog's name is "monster".
This pet is a dog, the name is "little pig".

The dog's name is "little pig".
The dog's name is "monster".
This pet is a dog, the name is "monster".

在示例1中,賦給pet變數的實際上是dog的一個副本,所以當dog設定了name的時候pet的name並沒發生改變。

在例項2中,賦給pet變數的是一個指標的副本,所以pet和dog一樣發生了編髮。

介面之間的組合

可以通過介面間的嵌入實現介面的組合。接⼝型別間的嵌⼊不會涉及⽅法間的“遮蔽”。只要組合的接⼝之間有同名的⽅法就會產⽣衝突,從⽽⽆ 法通過編譯,即使同名⽅法的簽名彼此不同也會是如此。

type Animal interface {
    // ScientificName 用於獲取動物的學名。
    ScientificName() string
    // Category 用於獲取動物的基本分類。
    Category() string
}

type Named interface {
    // Name 用於獲取名字。
    Name() string
}

type Pet interface {
    Animal
    Named
}

指標

哪些值是不可定址的

  1. 不可變的變數

如果一個變數是不可變的,那麼基於它的索引或切⽚的結果值都是不可定址的,因為即使拿到了這種值的記憶體地址也改變不了什麼。
如:

    const num = 123
    //_ = &num // 常量不可定址。
    //_ = &(123) // 基本型別值的字面量不可定址。

    var str = "abc"
    _ = str
    //_ = &(str[0]) // 對字串變數的索引結果值不可定址。
    //_ = &(str[0:2]) // 對字串變數的切片結果值不可定址。
    str2 := str[0]
    _ = &str2 // 但這樣的定址就是合法的。
  1. 臨時結果

在我們把臨時結果值賦給任何變數或常量之前,即使能拿到它的記憶體地址也是沒有任何意義的。所以也是不可定址的。

我們可以把各種對值字⾯量施加的表示式的求值結果都看做是 臨時結果。
如:
* ⽤於獲得某個元素的索引表示式。
* ⽤於獲得某個切⽚(⽚段)的切⽚表示式。
* ⽤於訪問某個欄位的選擇表示式。
* ⽤於調⽤某個函式或⽅法的調⽤表示式。
* ⽤於轉換值的型別的型別轉換表示式。
* ⽤於判斷值的型別的型別斷⾔表示式。
* 向通道傳送元素值或從通道那⾥接收元素值的接收表示式。

⼀個需要特別注意的例外是,對切⽚字⾯量的索引結果值是可定址的。因為不論怎樣,每個切⽚值都會持有⼀個底層陣列,⽽ 這個底層陣列中的每個元素值都是有⼀個確切的記憶體地址的。

//_ = &(123 + 456) // 算術操作的結果值不可定址。
//_ = &([3]int{1, 2, 3}[0]) // 對陣列字面量的索引結果值不可定址。
//_ = &([3]int{1, 2, 3}[0:2]) // 對陣列字面量的切片結果值不可定址。
_ = &([]int{1, 2, 3}[0]) // 對切片字面量的索引結果值卻是可定址的。
//_ = &([]int{1, 2, 3}[0:2]) // 對切片字面量的切片結果值不可定址。
//_ = &(map[int]string{1: "a"}[0]) // 對字典字面量的索引結果值不可定址。
  1. 不安全
    函式在Go語⾔中是⼀等公⺠,所以我們可以把代表函式或⽅法的字⾯量或識別符號賦給某個變數、傳給某個函式或者從某個函式傳出。

但是,這樣的函式和⽅法都是不可定址的。⼀個原因是函式就是程式碼,是不可變的。另⼀個原因是,拿到指向⼀段程式碼的指標是不安全的。

此外,對函式或⽅法的調⽤結果值也是不可定址的,這是因為它們都屬 於臨時結果。

如:

    //_ = &(func(x, y int) int {
    //  return x + y
    //}) // 字面量代表的函式不可定址。
    //_ = &(fmt.Sprintf) // 識別符號代表的函式不可定址。
    //_ = &(fmt.Sprintln("abc")) // 對函式的呼叫結果值不可定址。

goroutine協程

在Go語言中,協程是由go函式進行觸發的,當程式執⾏到⼀條go語句的時候,Go語⾔ 的運⾏時系統,會先試圖從某個存放空閒的G的佇列中獲取⼀個G(也就是goroutine),它只有在找不到空閒G的情況下才會 去建立⼀個新的G。

故已存在的goroutine總是會被優先復⽤。

在拿到了⼀個空閒的G之後,Go語⾔運⾏時系統會⽤這個G去包裝當前的那個go函式(或者說該函式中的那些程式碼),然後再 把這個G追加到某個存放可運⾏的G的佇列中。

在Go語⾔並不會去保證這些goroutine會以怎樣的順序運⾏。所以哪個goroutine先執⾏完、哪個goroutine後執⾏完往往是不可預知的,除⾮我們使⽤了某種Go語⾔提供的⽅式進⾏了⼈為 ⼲預。

所以,怎樣讓我們啟⽤的多個goroutine按照既定的順序運⾏?

多個goroutine按照既定的順序運⾏

下面我們先看個例子:

func main() {
    for i := 0; i < 10; i++ {
        go func() {
            fmt.Println(i)
        }()
    }
}

在下面的程式碼中,由於Go語言並不會按順序去執行排程,所以沒法知道fmt.Println(i)會在什麼時候被列印,也不知道fmt.Println(i)列印的時候i是多少,也有可能main方法執行完了,但是沒有一條輸出。

所以我們需要進行如下改造:

func main() {
    var count uint32
    trigger := func(i uint32, fn func()) {
        for {
            if n := atomic.LoadUint32(&count); n == i {
                fn()
                atomic.AddUint32(&count, 1)
                break
            }
            time.Sleep(time.Nanosecond)
        }
    }
    for i := uint32(0); i < 10; i++ {
        go func(i uint32) {
            fn := func() {
                fmt.Println(i)
            }
            trigger(i, fn)
        }(i)
    }
    trigger(10, func() {})
}

我們在for迴圈中聲明瞭一個fn函式,fn函式裡面只是簡單的執行列印i的值,然後傳入到trigger中。

trigger函式會不斷地獲取⼀個名叫count的變數的值,並判斷該值是否與引數i的值相同。如果相同,那麼就⽴即調⽤fn代 表的函式,然後把count變數的值加1,最後顯式地退出當前的迴圈。否則,我們就先讓當前的goroutine“睡眠”⼀個納秒再進 ⼊下⼀個迭代。

因為會有多個執行緒操作trigger函式,所以使用的count變數是通過原子操作來進行獲取值和加一操作。

所以過函式實際執行順序會根據count的值依次執行,這裡實現了一種自旋,未滿足條件的時候會不斷地進行檢查。

最後防止主協程在其他協程沒有執行完的時候就關閉,加上一個trigger(10, func() {})程式碼