Go 学习笔记(27)— type 关键字(类型定义、类型别名、类型查询、定义接口、定义结构体)
1. 類型別名定義
定義類型別名的寫法為:
type TypeAlias = Type
類型別名規定: TypeAlias 只是 Type 的別名,本質上 TypeAlias 與 Type 是同一個類型,就像一個孩子小時候有小名、乳名,上學后用學名,英語老師又會給他起英文名,但這些名字都指的是他本人。
2. 類型定義
類型定義語法如下:
type newType Type
其中 newType 是一種新的類型, newType 本身依然具備 Type 類型的特性。新類型與底層類型不能直接相互賦值和運算,如果需要,需要顯式轉換。
var m int = 5
var n int32 = 6
var a MyInt = MyInt(m) // ok
var a MyInt = MyInt(n) // ok
類型聲明語句一般出現在包一級,因此如果新創建的類型名字的首字符大寫,則在包外部也可以使用。
一個類型聲明語句創建了一個新的類型名稱,和現有類型具有相同的底層結構。新命名的類型提供了一個方法,用來分隔不同概念的類型,這樣即使它們底層類型相同也是不兼容的。
為了說明類型聲明,我們將不同溫度單位分別定義為不同的類型:
package tempconvtype Celsius float64 // 攝氏溫度
type Fahrenheit float64 // 華氏溫度const (AbsoluteZeroC Celsius = -273.15 // 絕對零度FreezingC Celsius = 0 // 結冰點溫度BoilingC Celsius = 100 // 沸水溫度
)func CToF(c Celsius) Fahrenheit {return Fahrenheit(c*9/5 + 32)
}func FToC(f Fahrenheit) Celsius {return Celsius((f - 32) * 5 / 9)
}
我們在這個包聲明了兩種類型: Celsius 和 Fahrenheit 分別對應不同的溫度單位。它們雖然有著相同的底層類型 float64 ,但是它們是不同的數據類型,因此它們不可以被相互比較或混在一個表達式運算。
刻意區分類型,可以避免一些像無意中使用不同單位的溫度混合計算導致的錯誤;因此需要一個類似 Celsius(t) 或 Fahrenheit(t) 形式的顯式轉型操作才能將 float64 轉為對應的類型。
Celsius(t) 和 Fahrenheit(t) 是類型轉換操作,它們并不是函數調用。類型轉換不會改變值本身,但是會使它們的語義發生變化。另一方面, CToF 和 FToC 兩個函數則是對不同溫度單位下的溫度進行換算,它們會返回不同的值。
對于每一個類型 T ,都有一個對應的類型轉換操作 T(x) ,用于將 x 轉為 T 類型(譯注:如果 T 是指針類型,可能會需要用小括弧包裝 T,比如(*int)(0))。
只有當兩個類型的底層基礎類型相同時,才允許這種轉型操作,或者是兩者都是指向相同底層結構的指針類型,這些轉換只改變類型而不會影響值本身。如果 x 是可以賦值給 T 類型的值,那么 x 必然也可以被轉為 T 類型,但是一般沒有這個必要。
數值類型之間的轉型也是允許的,并且在字符串和一些特定類型的 slice 之間也是可以轉換的。
例如,將一個浮點數轉為整數將丟棄小數部分,將一個字符串轉為[]byte類型的 slice 將拷貝一個字符串數據的副本。在任何情況下,運行時不會發生轉換失敗的錯誤(譯注: 錯誤只會發生在編譯階段)。
底層數據類型決定了內部結構和表達方式,也決定是否可以像底層類型一樣對內置運算符的支持。這意味著, Celsius 和 Fahrenheit 類型的算術運算行為和底層的 float64 類型是一樣的,正如我們所期望的那樣。
fmt.Printf("%g\n", BoilingC-FreezingC) // "100" °C
boilingF := CToF(BoilingC)
fmt.Printf("%g\n", boilingF-CToF(FreezingC)) // "180" °F
fmt.Printf("%g\n", boilingF-FreezingC) // compile error: type mismatch
比較運算符==和<也可以用來比較一個命名類型的變量和另一個有相同類型的變量,或有著相同底層類型的未命名類型的值之間做比較。但是如果兩個值有著不同的類型,則不能直接進行比較:
var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!
注意最后那個語句。盡管看起來像函數調用,但是 Celsius(f) 是類型轉換操作,它并不會改變值,僅僅是改變值的類型而已。測試為真的原因是因為 c 和 g 都是零值。
一個命名的類型可以提供書寫方便,特別是可以避免一遍又一遍地書寫復雜類型(譯注:例如用匿名的結構體定義變量)。雖然對于像 float64 這種簡單的底層類型沒有簡潔很多,但是如果是復雜的類型將會簡潔很多,特別是我們即將討論的結構體類型。
命名類型還可以為該類型的值定義新的行為。這些行為表示為一組關聯到該類型的函數集合,我們稱為類型的方法集。
下面的聲明語句, Celsius 類型的參數 c 出現在了函數名的前面,表示聲明的是 Celsius 類型的一個名叫 String 的方法,該方法返回該類型對象 c 帶著 °C 溫度單位的字符串:
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c)
}
許多類型都會定義一個 String 方法,因為當使用 fmt 包的打印方法時,將會優先使用該類型對應的 String 方法返回的結果打印。
c := FToC(212.0)
fmt.Println(c.String()) // "100°C"
fmt.Printf("%v\n", c) // "100°C"; no need to call String explicitly
fmt.Printf("%s\n", c) // "100°C"
fmt.Println(c) // "100°C"
fmt.Printf("%g\n", c) // "100"; does not call String
fmt.Println(float64(c)) // "100"; does not call String
3. 類型別名與類型定義差異
類型別名與類型定義表面上看只有一個等號的差異,那么它們之間實際的區別有哪些呢?下面通過一段代碼來理解。
package mainimport ("fmt"
)// 將NewInt定義為int類型
// 通過 type 關鍵字的定義,NewInt 會形成一種新的類型,NewInt 本身依然具備 int 類型的特性。
type NewInt int// 將int取一個別名叫IntAlias, 將 IntAlias 設置為 int 的一個別名,使 IntAlias 與 int 等效。
type IntAlias = intfunc main() {// 將a聲明為NewInt類型var a NewInt// 查看a的類型名fmt.Printf("a type: %T\n", a) // a type: main.NewInt// 將 b 聲明為IntAlias類型var b IntAlias// 查看b的類型名fmt.Printf("b type: %T\n", b) // b type: int
}
結果顯示 a 的類型是 main.NewInt ,表示 main 包下定義的 NewInt 類型,b 類型是 int , IntAlias 類型只會在代碼中存在,編譯完成時,不會有 IntAlias 類型。
4. 非本地類型不能定義方法
能夠隨意地為各種類型起名字,是否意味著可以在自己包里為這些類型任意添加方法呢?參見下面的代碼演示:
package mainimport ("time"
)// 定義time.Duration的別名為MyDuration
type MyDuration = time.Duration// 為 MyDuration 添加一個方法
func (m MyDuration) EasySet(a string) {}func main() {}
錯誤信息:
./hello.go:11:6: cannot define new methods on non-local type time.Duration
編譯器提示:不能在一個非本地的類型 time.Duration 上定義新方法,非本地類型指的就是 time.Duration 不是在 main 包中定義的,而是在 time 包中定義的,與 main 包不在同一個包中,因此不能為不在一個包中的類型定義方法。
修改方案為將第 8 行類型別名修改為類型定義,如下:
type MyDuration time.Duration
5. 在結構體成員嵌入時使用別名
當類型別名作為結構體嵌入的成員時會發生什么情況呢?請參考下面的代碼。
package mainimport ("fmt""reflect"
)// 定義商標結構
type Brand struct {
}// 為商標結構添加Show()方法
func (t Brand) Show() {
}// 為Brand定義一個別名FakeBrand
type FakeBrand = Brand// 定義車輛結構
type Vehicle struct {// 嵌入兩個結構FakeBrandBrand
}func main() {// 聲明變量a為車輛類型var a Vehicle// 指定調用FakeBrand的Showa.FakeBrand.Show()// 取a的類型反射對象ta := reflect.TypeOf(a)// 遍歷a的所有成員for i := 0; i < ta.NumField(); i++ {// a的成員信息f := ta.Field(i)// 打印成員的字段名和類型fmt.Printf("FieldName: %v, FieldType: %v\n", f.Name, f.Type.Name())}
}
輸出結果:
FieldName: FakeBrand, FieldType: Brand
FieldName: Brand, FieldType: Brand
這個例子中,FakeBrand 是 Brand 的一個別名,在 Vehicle 中嵌入 FakeBrand 和 Brand 并不意味著嵌入兩個 Brand,FakeBrand 的類型會以名字的方式保留在 Vehicle 的成員中。
如果嘗試將第 33 行改為:
a.Show()
編譯器將發生報錯:
ambiguous selector a.Show
在調用 Show() 方法時,因為兩個類型都有 Show() 方法,會發生歧義,證明 FakeBrand 的本質確實是 Brand 類型。
6. 函數也是類型,可以作為參數傳遞給別的函數
package maintype math func(int, int) int //定義一個函數類型,兩個 int 參數,一個 int 返回值//定義一個函數 add,這個函數兩個 int 參數一個 int 返回值,與 math 類型相符
func add(i int, j int) int {return i + j
}//再定義一個 multiply,這個函數同樣符合 math 類型
func multiply(i, j int) int {return i * j
}//foo 函數,需要一個 math 類型的參數,用 math 類型的函數計算第 2 和第 3 個參數數字,并返回計算結果
//稍后在 main 中我們將 add 函數和 multiply 分別作為參數傳遞給它
func foo(m math, n1, n2 int) int {return m(1, 2)
}func main() {//傳遞 add 函數和兩個數字,計算相加結果n := foo(add, 1, 2)println(n)//傳遞 multply 和兩個數字,計算相乘結果n = foo(multiply, 1, 2)println(n)
}
7. type 類型用法
type 有如下幾種用法:
- 定義結構體
- 定義接口
- 類型定義
- 類型別名
- 類型查詢
7.1 定義結構體
結構體是用戶自定義的一種抽象的數據結構, Golang 中 struct 類似于 Java 語言中的 class ,在程序設計中,有著舉足輕重的地位。結構體的用法,將會在 struct 關鍵字中詳細的介紹。下邊來看一下定義一個結構體的語法格式:
type name struct {Field1 dataTypeField2 dataTypeField3 dataType
}
7.2 定義接口
接口相關知識點,將會在 interface 關鍵字中詳細介紹,下邊來看一段定義接口的語法格式:
type name interface{Read()Write()
}
7.3 類型定義
使用類型定義定義出來的類型與原類型不相同,所以不能使用新類型變量賦值給原類型變量,除非使用強制類型轉換。下面來看一段示例代碼,根據 string 類型,定義一種新的類型,新類型名稱是 name :
type name string
為什么要使用類型定義呢?
類型定義可以在原類型的基礎上創造出新的類型,有些場合下可以使代碼更加簡潔,如下邊示例代碼:
package main
import ("fmt"
)
// 定義一個接收一個字符串類型參數的函數類型
type handle func(str string)
// exec函數,接收handle類型的參數
func exec(f handle) {f("hello")
}
func main() {// 定義一個函數類型變量,這個函數接收一個字符串類型的參數var p = func(str string) {fmt.Println("first", str)}exec(p)// 匿名函數作為參數直接傳遞給exec函數exec(func(str string) {fmt.Println("second", str)})
}
輸出信息是:
first hello
second hello
上邊的示例是類型定義的一種簡單應用場合,如果不使用類型定義,那么想要實現上邊示例中的功能,應該怎么書寫這段代碼呢?
// exec函數,接收handle類型的參數
func exec(f func(str string)) {f("hello")
}
exec 函數中的參數類型,需要替換成 func(str string) 了,咋一看去也不復雜,但是假如 exec 接收一個需要 5 個參數的函數變量呢?是不是感覺參數列表就會很長了。
func exec(f func(str string, str2 string, num int, money float64, flag bool)) {f("hello")
}
從上邊的代碼可以發現, exec 函數的參數列表可讀性變差了。下邊再來看看使用類型定義是怎么實現這個功能:
package main
import ("fmt"
)
// 定義一個需要五個參數的函數類型
type handle func(str string, str2 string, num int, money float64, flag bool)
// exec函數,接收handle類型的參數
func exec(f handle) {f("hello", "world", 10, 11.23, true)
}
func demo(str string, str2 string, num int, money float64, flag bool) {fmt.Println(str, str2, num, money, flag)
}
func main() {exec(demo)
}
7.4 類型別名
類型別名這個特性在 Golang 1.9 中引入。使用類型別名定義出來的類型與原類型一樣,即可以與原類型變量互相賦值,又擁有了原類型的所有方法集。給 strng 類型取一個別名,別名名稱是 name :
type name = string
類型別名與類型定義不同之處在于,使用類型別名需要在別名和原類型之間加上賦值符號( = );使用類型別名定義的類型與原類型等價,而使用類型定義出來的類型是一種新的類型。
如下邊示例:
package main
import ("fmt"
)
type a = string
type b string
func SayA(str a) {fmt.Println(str)
}
func SayB(str b) {fmt.Println(str)
}
func main() {var str = "test"SayA(str)//錯誤參數傳遞,str是字符串類型,不能賦值給b類型變量SayB(str)
}
這段代碼在編譯時會出現如下錯誤:
.\main.go:21:6: cannot use str (type string) as type b in argument to SayB
從錯誤信息可知, str 為字符串類型,不能當做 b 類型參數傳入 SayB 函數中。而 str 卻可以當做 a 類型參數傳入到 SayA 函數中。由此可見,使用類型別名定義的類型與原類型一致,而類型定義定義出來的類型,是一種新的類型。
給類型別名新增方法,會添加到原類型方法集中
給類型別名新增方法后,原類型也能使用這個方法。下邊請看一段示例代碼:
package main
import ("fmt"
)
// 根據string類型,定義類型S
type S string
func (r *S) Hi() {fmt.Println("S hi")
}
// 定義S的類型別名為T
type T = S
func (r *T) Hello() {fmt.Println("T hello")
}
// 函數參數接收S類型的指針變量
func exec(obj *S) {obj.Hello()obj.Hi()
}
func main() {t := new(T)s := new(S)exec(s)// 將T類型指針變量傳遞給S類型指針變量exec(t)
}
輸出信息是:
T hello
S hi
T hello
S hi
上邊的示例中,S 是原類型,T 是 S 類型別名。在給 T 增加了 Hello 方法后,S 類型的變量也可以使用 Hello 方法。說明給類型別名新增方法后,原類型也能使用這個方法。從示例中可知,變量 t 可以賦值給 S 類型變量 s,所以類型別名是給原類型取了一個小名,本質上沒有發生任何變化。
類型別名,只能對同一個包中的自定義類型產生作用。舉個例子,Golang SDK 中有很多個包,是不是我們可以使用類型別名,給 SDK 包中的結構體類型新增方法呢?答案是:不行。請牢記一點:類型別名,只能對包內的類型產生作用,對包外的類型采用類型別名,在編譯時將會提示如下信息:
cannot define new methods on non-local type string
7.5 類型查詢
類型查詢,就是根據變量,查詢這個變量的類型。為什么會有這樣的需求呢?
Goalng 中有一個特殊的類型 interface{} ,這個類型可以被任何類型的變量賦值,如果想要知道到底是哪個類型的變量賦值給了 interface{} 類型變量,就需要使用類型查詢來解決這個需求,示例代碼如下:
package main
import ("fmt"
)
func main() {// 定義一個interface{}類型變量,并使用string類型值”abc“初始化var a interface{} = "abc"// 在switch中使用 變量名.(type) 查詢變量是由哪個類型數據賦值。switch v := a.(type) {case string:fmt.Println("字符串")case int:fmt.Println("整型")default:fmt.Println("其他類型", v)}
}
如果使用 .(type) 查詢類型的變量不是 interface{} 類型,則在編譯時會報如下錯誤:
cannot type switch on non-interface value a (type string)
如果在 switch 以外地方使用 .(type) ,則在編譯時會提示如下錯誤:
use of .(type) outside type switch
所以,使用 type 進行類型查詢時,只能在 switch 中使用,且使用類型查詢的變量類型必須是 interface{} 。
總結
以上是生活随笔為你收集整理的Go 学习笔记(27)— type 关键字(类型定义、类型别名、类型查询、定义接口、定义结构体)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 百开头的成语接龙大全
- 下一篇: 《题山石榴花》是哪个时期的作品?