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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM