Go有一个非常有趣的类型系统。 它避开了类和继承,而采用了接口和组合,但是另一方面,它没有模板或泛型。 它处理集合的方式也很独特。
在本教程中,您将了解Go类型系统的来龙去脉,以及如何有效地使用它来编写清晰且惯用的Go代码。
Go类型系统的全景图
Go类型系统支持过程,面向对象和功能范例。 它对通用编程的支持非常有限。 虽然Go是绝对静态的语言,但它确实通过接口,一流的函数和反射为动态技术提供了足够的灵活性。 Go的类型系统缺少大多数现代语言中常见的功能:
- 由于Go的错误处理基于返回码和错误接口,因此没有异常类型。
- 没有操作员超载。
- 没有函数重载(具有不同参数的相同函数名称)。
- 没有可选或默认功能参数。
这些遗漏都是为了使Go尽可能简单而设计的。
类型别名
您可以在Go中命名类型并创建不同的类型。 如果不进行转换,则不能将基础类型的值分配给别名类型。 例如,以下程序中的赋值var b int = a
会导致编译错误,因为类型Age
是int的别名,但不是 int:
package main
type Age int
func main() {
var a Age = 5
var b int = a
}
Output:
tmp/sandbox547268737/main.go:8: cannot use a (type Age) as
type int in assignment
您可以将类型声明分组或每行使用一个声明:
type IntIntMap map[int]int
StringSlice []string
type (
Size uint64
Text string
CoolFunc func(a int, b bool)(int, error)
)
基本类型
所有常见的怀疑都在这里:布尔值,字符串,具有显式位大小的整数和无符号整数,浮点数(32位和64位)和复数(64位和128位)。
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32, represents a Unicode code point
float32 float64
complex64 complex128
弦乐
Go中的字符串是UTF8编码的,因此可以表示任何Unicode字符。 字符串包提供了许多字符串操作。 这是一个使用单词数组,将它们转换为适当大小写并将它们连接到句子的示例。
package main
import (
"fmt"
"strings"
)
func main() {
words := []string{"i", "LikE", "the ColORS:", "RED,",
"bLuE,", "AnD", "GrEEn"}
properCase := []string{}
for i, w := range words {
if i == 0 {
properCase = append(properCase, strings.Title(w))
} else {
properCase = append(properCase, strings.ToLower(w))
}
}
sentence := strings.Join(properCase, " ") + "."
fmt.Println(sentence)
}
指针
Go有指针。 空指针(请参阅后面的零值)为nil。 您可以使用&
运算符获取指向值的指针,然后使用*
运算符获取指针。 您也可以具有指向指针的指针。
package main
import (
"fmt"
)
type S struct {
a float64
b string
}
func main() {
x := 5
px := &x
*px = 6
fmt.Println(x)
ppx := &px
**ppx = 7
fmt.Println(x)
}
面向对象编程
Go通过接口和结构支持面向对象的编程。 没有类,没有类层次结构,尽管您可以在提供某种单一继承的结构中嵌入匿名结构。
要详细了解Go中的面向对象编程,请查看Let's Go:Golang中的面向对象编程 。
介面
接口是Go类型系统的基石。 接口只是方法签名的集合。 实现所有方法的每种类型都与接口兼容。 这是一个简单的例子。 Shape
接口定义了两个方法: GetPerimeter()
和GetArea()
。 Square
对象实现接口。
type Shape interface {
GetPerimeter() uint
GetArea() uint
}
type Square struct {
side uint
}
func (s *Square) GetPerimeter() uint {
return s.side * 4
}
func (s *Square) GetArea() uint {
return s.side * s.side
}
空接口interface{}
与任何类型都兼容,因为不需要任何方法。 然后,空接口可以指向任何对象(类似于Java的Object或C / C ++ void指针),通常用于动态类型化。 接口始终是指针,并且始终指向具体对象。
有关Go接口的完整文章,请查看: 如何定义和实现Go接口 。
结构
结构是Go的用户定义类型。 结构包含命名字段,可以是基本类型,指针类型或其他结构类型。 您还可以将结构匿名嵌入到其他结构中,作为实现继承的一种形式。
在以下示例中,S1和S2结构嵌入在S3结构中,该结构也具有自己的int
字段和指向其自身类型的指针:
package main
import (
"fmt"
)
type S1 struct {
f1 int
}
type S2 struct {
f2 int
}
type S3 struct {
S1
S2
f3 int
f4 *S3
}
func main() {
s := &S3{S1{5}, S2{6}, 7, nil}
fmt.Println(s)
}
Output:
&{{5} {6} 7 <nil>}
类型断言
类型断言允许您将接口转换为其具体类型。 如果您已经知道底层类型,则只需声明它即可。 如果不确定,可以尝试几种类型断言,直到找到正确的类型。
在下面的示例中,有一个列表,其中包含字符串和非字符串值,这些值表示为空接口的一部分。 代码遍历所有内容,尝试将每个项目转换为字符串并将所有字符串存储在最终打印的单独切片中。
package main
import "fmt"
func main() {
things := []interface{}{"hi", 5, 3.8, "there", nil, "!"}
strings := []string{}
for _, t := range things {
s, ok := t.(string)
if ok {
strings = append(strings, s)
}
}
fmt.Println(strings)
}
Output:
[hi there !]
反射
使用Go reflect
包,您可以直接检查接口的类型而无需类型声明。 如果需要,您还可以提取接口的值并将其转换为接口(不太有用)。
这是一个与前面的示例类似的示例,但是除了打印字符串外,它还只是对它们进行计数,因此无需从interface{}
转换为string
。 关键是调用reflect.Type()
以获取类型对象,该对象具有Kind()
方法,该方法使我们能够检测是否正在处理字符串。
package main
import (
"fmt"
"reflect"
)
func main() {
things := []interface{}{"hi", 5, 3.8, "there", nil, "!"}
stringCount := 0
for _, t := range things {
tt := reflect.TypeOf(t)
if tt != nil && tt.Kind() == reflect.String {
stringCount++
}
}
fmt.Println("String count:", stringCount)
}
功能
功能是Go中的一等公民。 这意味着您可以将函数分配给变量,将函数作为参数传递给其他函数,或将它们作为结果返回。 这使您可以在Go中使用功能编程风格。
下面的示例演示了几个函数GetUnaryOp()
和GetBinaryOp()
,这些函数返回随机选择的匿名函数。 主程序根据参数的数量决定是否需要一元运算或二进制运算。 它将所选函数存储在名为“ op”的局部变量中,然后使用正确数量的参数调用它。
package main
import (
"fmt"
"math/rand"
)
type UnaryOp func(a int) int
type BinaryOp func(a, b int) int
func GetBinaryOp() BinaryOp {
if rand.Intn(2) == 0 {
return func(a, b int) int { return a + b }
} else {
return func(a, b int) int { return a - b }
}
}
func GetUnaryOp() UnaryOp {
if rand.Intn(2) == 0 {
return func(a int) int { return -a }
} else {
return func(a int) int { return a * a }
}
}
func main() {
arguments := [][]int{{4,5},{6},{9},{7,18},{33}}
var result int
for _, a := range arguments {
if len(a) == 1 {
op := GetUnaryOp()
result = op(a[0])
} else {
op := GetBinaryOp()
result = op(a[0], a[1])
}
fmt.Println(result)
}
}
频道
通道是一种异常的数据类型。 您可以将它们视为用于在goroutine之间传递消息的消息队列。 通道是强类型的。 它们是同步的,并且具有用于发送和接收消息的专用语法支持。 每个通道可以是仅接收,仅发送或双向的。
通道也可以选择进行缓冲。 您可以使用范围对通道中的消息进行迭代,并且使用通用选择操作可以在多个通道上同时阻止执行例程。
这是一个典型的示例,其中整数列表的平方和由两个go例程并行计算,每个例程负责该列表的一半。 main函数等待两个go例程的结果,然后将总计的部分和相加。 注意如何使用内置函数make()
创建通道c
,以及代码如何通过特殊的<-
运算符从通道读取和写入通道。
package main
import "fmt"
func sum_of_squares(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v * v
}
c <- sum // send sum to c
}
func main() {
s := []int{11, 32, 81, -9, -14}
c := make(chan int)
go sum_of_squares(s[:len(s)/2], c)
go sum_of_squares(s[len(s)/2:], c)
sum1, sum2 := <-c, <-c // receive from c
total := sum1 + sum2
fmt.Println(sum1, sum2, total)
}
馆藏
Go有几个内置的通用集合,可以存储任何类型。 这些集合很特殊,您不能定义自己的通用集合。 集合是数组,切片和地图。 通道也是通用的,也可以视为集合,但是它们具有一些非常独特的属性,因此我倾向于单独讨论它们。
数组
数组是相同类型元素的固定大小的集合。 这是一些数组:
package main
import "fmt"
func main() {
a1 := [3]int{1, 2, 3}
var a2 [3]int
a2 = a1
fmt.Println(a1)
fmt.Println(a2)
a1[1] = 7
fmt.Println(a1)
fmt.Println(a2)
a3 := [2]interface{}{3, "hello"}
fmt.Println(a3)
}
数组的大小是其类型的一部分。 您可以复制相同类型和大小的阵列。 该副本是按值复制的。 如果要存储其他类型的项目,则可以使用空接口数组的转义填充。
切片
数组由于其固定大小而受到很大限制。 切片更有趣。 您可以将切片视为动态数组。 在幕后,切片使用数组来存储其元素。 您可以检查切片的长度,追加元素和其他切片,并且最有趣的是,您可以提取类似于Python切片的子切片:
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3}
var s2 []int
s2 = s1
fmt.Println(s1)
fmt.Println(s2)
// Modify s1
s1[1] = 7
// Both s1 and s2 point to the same underlying array
fmt.Println(s1)
fmt.Println(s2)
fmt.Println(len(s1))
// Slice s1
s3 := s1[1:len(s1)]
fmt.Println(s3)
}
复制切片时,只需将引用复制到相同的基础数组。 切片时,子切片仍指向同一阵列。 但是,当您追加时,会得到一个指向新数组的切片。
您可以使用带有索引的常规循环或使用范围来遍历数组或切片。 您还可以使用make()
函数以给定的容量创建切片,并使用其数据类型的零值对其进行初始化:
package main
import "fmt"
func main() {
// Create a slice of 5 booleans initialized to false
s1 := make([]bool, 5)
fmt.Println(s1)
s1[3] = true
s1[4] = true
fmt.Println("Iterate using standard for loop with index")
for i := 0; i < len(s1); i++ {
fmt.Println(i, s1[i])
}
fmt.Println("Iterate using range")
for i, x := range(s1) {
fmt.Println(i, x)
}
}
Output:
[false false false false false]
Iterate using standard for loop with index
0 false
1 false
2 false
3 true
4 true
Iterate using range
0 false
1 false
2 false
3 true
4 true
地图
映射是键值对的集合。 您可以为其分配地图文字或其他地图。 您还可以使用make
内置函数创建空地图。 您可以使用方括号访问元素。 Maps使用range
支持迭代,您可以通过尝试访问键并检查第二个可选的布尔返回值来测试键是否存在。
package main
import (
"fmt"
)
func main() {
// Create map using a map literal
m := map[int]string{1: "one", 2: "two", 3:"three"}
// Assign to item by key
m[5] = "five"
// Access item by key
fmt.Println(m[2])
v, ok := m[4]
if ok {
fmt.Println(v)
} else {
fmt.Println("Missing key: 4")
}
for k, v := range m {
fmt.Println(k, ":", v)
}
}
Output:
two
Missing key: 4
5 : five
1 : one
2 : two
3 : three
请注意,迭代不是按照创建或插入顺序进行的。
零值
Go中没有未初始化的类型。 每种类型都有一个预定义的零值。 如果声明了类型的变量但未为其分配值,则该变量将包含其零值。 这是重要的类型安全功能。
对于任何T
类型, *new(T)
将返回T
一个零值。
对于布尔类型,零值为“ false”。 对于数字类型,零值为...零。 对于切片,地图和指针,它为零。 对于结构,这是所有字段都初始化为零值的结构。
package main
import (
"fmt"
)
type S struct {
a float64
b string
}
func main() {
fmt.Println(*new(bool))
fmt.Println(*new(int))
fmt.Println(*new([]string))
fmt.Println(*new(map[int]string))
x := *new([]string)
if x == nil {
fmt.Println("Uninitialized slices are nil")
}
y := *new(map[int]string)
if y == nil {
fmt.Println("Uninitialized maps are nil too")
}
fmt.Println(*new(S))
}
模板或泛型呢?
去没有。 这可能是对Go的类型系统最常见的抱怨。 Go设计师对此想法持开放态度,但是在不违反该语言背后的其他设计原则的前提下,尚不知道如何实现它。 如果您急需一些通用数据类型该怎么办? 这里有一些建议:
- 如果只有一些实例化,请考虑仅创建具体对象。
- 使用一个空接口(有时需要将assert键入回具体类型)。
- 使用代码生成。
结论
Go有一个有趣的类型系统。 Go设计师做出了明确的决定,以保持简单的一面。 如果您对Go编程很认真,则应该花一些时间并了解它的类型系统及其特质。 这将非常值得您花时间。
翻译自: https://code.tutsplus.com/tutorials/deep-dive-into-the-go-type-system--cms-29065