Java程序員學習Go指南(二)

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

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⽅法。dom

在結構體中聲明一個嵌入字段

由於在Go中是沒有繼承一說,因此使用了嵌入字段的方式來實現類型之間的組合,實現了方法的重用。函數

這裏繼續用到上面的結構體AnimalCategoryui

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

字段聲明AnimalCategory表明了Animal類型的⼀個嵌⼊字段。Go語⾔規範規定,若是⼀個字段 的聲明中只有字段的類型名⽽沒有字段的名稱,那麼它就是⼀個嵌⼊字段,也能夠被稱爲匿名字段。嵌⼊字段的類型既是類型也是名稱。atom

若是要像java中引用字段裏面的屬性,那麼能夠這麼寫:線程

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

這裏仍是和java是同樣的,可是接下來要講的卻和java有很大區別指針

因爲咱們在AnimalCategory中寫了一個String的方法,若是咱們沒有給Animal寫String的方法,那麼咱們直接打印會獲得什麼結果?code

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() {})代碼。

相關文章
相關標籤/搜索