本文的内容建立在已对 C 和 C++ 熟练掌握的基础之上,主要给出 Go 与 C 和 C++ 之间的不同之处。
Go 中指针的定义方式和 C 类似,基本定义方式为 var ptr *type
,一些示例如下。
1 | var p1 *int // 指向 int 的指针 |
Go 中的取地址操作与 C 中一致,使用 &
操作符,未赋值的指针自动指向 nil
(空),示例如下。
1 | var x int = 10 |
Go 中的变量类型分为值类型及引用类型(指针类型),值类型包含有 bool
、数字类型、string
和array
,引用类型有 slice
、map
、channel
、interface
、function
。
值类型意味着,当你将其直接传入函数时,所作的修改不会反映到原本的值,而引用类型则会反映到原本的值。Go 中引用类型的底层实现实际为一个带有指针的结构体(典型的就是slice
),所以在即使是传值也可以改变原值。
make
和 new
new
用于值类型的创建,类似于 C++ 中的 new
,其会返回该类型的指针,示例如下。
1 | p1 := new(int) // -> 0 |
new
得到的 complex64
/ complex128
和 string
指针无法直接对原值修改,这是由 Go 中只读的特性决定的。
make
用于引用类型的创建,其会返回该类型的值,示例如下。
1 | p1 := make([]int, 3, 5) // []int, len = 3, cap = 5 |
关于通道类型暂且不表。
Go 中函数的定义方式类似于 C,基本定义方式为 func func_name([param list]) ([return list]) {}
,支持多个返回值。
1 | func foo(x, y int) (int, int) { // (int, int) 为返回列表 |
Go 中可以使用函数作为参数。
1 | func foo(x int) int { |
函数 foo()
作为参数传入 bar()
函数,注意这里对函数类型的描述方法与格式。
Go 中可以使用函数作为返回值,这种方式返回的是匿名函数,可作为闭包。
在 Go 语言中,闭包是指一个函数可以引用其外部作用域的变量,即使该作用域已经结束,变量仍然可以被这个函数访问和修改。换句话说,闭包是带有状态的函数。
1 | func foo(i int) func(int) int { |
Go 中使用结构体实现 C 中类的作用,使用函数实现类的方法,与普通函数的差别是 func
和函数名之间需要加上结构体传入接收者,下面是一个类方法的实现示例。
1 | type Student struct { |
结构体传入时的命名建议为结构体名的第一个小写字母,例如上面的结构体 Student
,那么传入的参数名建议为 s
。
对上面的代码稍加修改,
x := &Student{"1", "casey", 0}
,上面的代码不会出错。对于接收者,Go 会对值类型按需求自动取地址,而对指针类型会按需求自动解引用。注意,仅对结构体方法的接收者起作用。在 Go 中将结构体作为参数同样有像 C 中那样的拷贝开销,因而需要考虑传入结构体的值还是址。
Go 中定义接口的一般方式如下。
1 | type interface_name interface { |
Go 中的接口实现了一些方法,任何其他类型只要实现了这些方法就是实现了这个接口,没有显式声明。其实可以类比于 C 中没有成员变量,只有成员函数,且成员函数都是纯虚函数的基类,接口就是一个基类指针,其他实现了纯虚函数的类型即继承了这个基类,从而实现了 Go 中的继承与多态。
接口变量实际包含两个部分,一是变量的类型,二是变量本身的值。
以下是一个接口的示例。首先定义一个接口,含有两个方法 Area()
和 Perimeter()
分别求面积和周长。然后定义两个结构体 Circle
和 Rectangle
实现接口的方法,最后通过接口实现对方法的调用。
1 | type Shape interface { |
注意上述代码的两个小细节:
nil
;c
为例,有 s = c
和 s = &c
两种,这主要取决于实现接口时用的方法传入的结构体是值还是址。若像上面这样传入结构体的值 func (c Circle) Area() float64 {}
,那么使用两种均可(值与址调用),s = &c
会自动解引用;若是传入址 func (c *Circle) Area() float64 {}
,则只能用 s = &c
来调用方法(仅能址调用)。这里的情况注意要和结构体的方法进行区分。空接口 interface {}
是 Go 中的一种特殊接口,任意类型都实现了空接口,这也就意味着空接口可以指向任何类型,但是无法调用任何方法。
类型断言的作用是对一个接口指定某种类型抽取其底层值,基本语法为 val := interface_val.(type)
。例如,在上面的接口例子中,将 func main() {}
做些修改。
1 | func main() { |
r := s.(Rectangle)
即是类型断言,其将 s
底层值指定以 Rectangle
类型抽出。但是显然存在错误,类型不匹配,触发 panic
。
为了避免这种问题,应使用带检查的断言,在断言前实现检查,如下。
1 | func main() { |
type switch 是一种特殊的 switch
语句,其只作用于接口,用于判断接口当前值的类型。示例如下。
1 | var s Shape = Circle{radius: 5} |
type switch 的同时可以取值,写成 switch val := s.(type) {...}
,得到对应类型的值 val
。
接口可以嵌套,但是接口的嵌套在实现上并没有特殊性,因为接口中都是虚函数,只需要依次实现嵌套的接口中所有的方法即实现了该嵌套的接口。以下是一个示例,A
接口嵌套了 B
及 C
接口,结构体 D
实现了 A
中所有的方法。
1 | type A interface { |
以下面的代码为例。
1 | type A struct { |
注意两个细节,b
的初始化方式与 b.add()
这种调用方式。可以发现,结构体 B
中嵌套了一个匿名结构体变量 A
,而 A
中又有变量 val
,此时可以直接使用 b.val
调用该变量,类似的,也就可以直接调用匿名结构体变量的方法。匿名嵌套的写法会使所有 A
中的字段与方法提升到 B
的作用域,这种行为非常类似于继承。
再看一个例子。
1 | type A struct { |
这个例子在上一个例子的基础上将 A
中的变量也置为了匿名变量,同时 A
和 B
存在同名方法 add()
。由于匿名结构体 A
的嵌套,使 A
中的 int
被提升到 B
的作用域,此时的 B
中存在有两个匿名 int
变量,但这是合法的,并不会报错。此时,我们需要指明字段名才能访问到 A
中的匿名变量。类似的,当 B
和 A
中存在有相同的方法时,需要指明字段名才能正确调用。
除了上面介绍的可以直接 return [return list]
之外,还可以确定的指明返回的字段名,然后直接 return
,示例如下。
1 | func addsub(a, b int) (c, d int) { |
这样明确给返回值赋值,可以不用在返回时指明返回值。
接口既能接收值也能接收址,只要该变量能调用接口中的函数,那么就认为他可以被接口接收,接口可以赋值为该变量。
一旦某个对象被存入了接口,之后通过该接口只能调用接口中的方法,对象本身的方法无法调用,若想要调用,需要使用类型断言进行转换。