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
}
指標
哪些值是不可定址的
- 不可變的變數
如果一個變數是不可變的,那麼基於它的索引或切⽚的結果值都是不可定址的,因為即使拿到了這種值的記憶體地址也改變不了什麼。
如:
const num = 123
//_ = &num // 常量不可定址。
//_ = &(123) // 基本型別值的字面量不可定址。
var str = "abc"
_ = str
//_ = &(str[0]) // 對字串變數的索引結果值不可定址。
//_ = &(str[0:2]) // 對字串變數的切片結果值不可定址。
str2 := str[0]
_ = &str2 // 但這樣的定址就是合法的。
- 臨時結果
在我們把臨時結果值賦給任何變數或常量之前,即使能拿到它的記憶體地址也是沒有任何意義的。所以也是不可定址的。
我們可以把各種對值字⾯量施加的表示式的求值結果都看做是 臨時結果。
如:
* ⽤於獲得某個元素的索引表示式。
* ⽤於獲得某個切⽚(⽚段)的切⽚表示式。
* ⽤於訪問某個欄位的選擇表示式。
* ⽤於調⽤某個函式或⽅法的調⽤表示式。
* ⽤於轉換值的型別的型別轉換表示式。
* ⽤於判斷值的型別的型別斷⾔表示式。
* 向通道傳送元素值或從通道那⾥接收元素值的接收表示式。
⼀個需要特別注意的例外是,對切⽚字⾯量的索引結果值是可定址的。因為不論怎樣,每個切⽚值都會持有⼀個底層陣列,⽽ 這個底層陣列中的每個元素值都是有⼀個確切的記憶體地址的。
//_ = &(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]) // 對字典字面量的索引結果值不可定址。
- 不安全
函式在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() {})
程式碼