7

【基础】Golang基本语法二(数组、方法和接口)

 2 years ago
source link: https://wintrysec.github.io/2021/09/26/%E3%80%905%E3%80%91%E5%AE%89%E5%85%A8%E5%BC%80%E5%8F%91/Golang/%E3%80%90%E5%9F%BA%E7%A1%80%E3%80%91Golang%E5%9F%BA%E7%A1%80%E8%AF%AD%E6%B3%95%E4%BA%8C%EF%BC%88%E5%87%BD%E6%95%B0%E3%80%81%E6%96%B9%E6%B3%95%E5%92%8C%E6%8E%A5%E5%8F%A3%EF%BC%89/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【基础】Golang基本语法二(数组、方法和接口)

类型 [n]T 表示拥有 nT 类型的值的数组。

var a [10]int
将变量 a 声明为拥有 10 个整数的数组

数组的长度是其类型的一部分,因此数组不能改变大小。这看起来是个限制,不过没关系,Go 提供了更加便利的方式来使用数组。

package main

import fmt

func main() {
var a [2]string
a[0] = Hello
a[1] = World
fmt.Println(a[0], a[1])
fmt.Println(a)

primes = [6]int{1, 2, 3, 4, 5, 6}
fmt.Println(primes)
}
Hello World
[Hello World]
[1 2 3 4 5 6]

每个数组的大小都是固定的,而切片则为数组元素提供动态大小的、灵活的视角。

在实践中,切片比数组更常用。

类型 []T 表示一个元素类型为 T 的切片。

切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:

a[low  high]

它会选择一个半开区间,包括第一个元素,但排除最后一个元素。

以下表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素:

a[14]
package main

import fmt

func main() {
primes = [6]int{1, 2, 3, 4, 5, 6}

var s []int = primes[14]
fmt.Println(s)
fmt.Printf(切片长度为:%d, len(s)) 长度等于元素个数,上界-下界=元素个数
fmt.Printf(切片容量为:%d, cap(s))
}
[2 3 4]
切片长度为:3
切片容量为:5

切片并不存储任何数据,它只是描述了底层数组中的一段。

更改切片的元素会修改其底层数组中对应的元素,与它共享底层数组的切片都会观测到这些修改。

package main

import fmt

func main() {
names = [4]string{
John,
Paul,
George,
Ringo,
} 这个是数组
fmt.Println(names)

a = names[02] 这是个切片
b = names[13] 这是个切片
fmt.Println(a, b)

b[0] = XXX
fmt.Println(a, b)
fmt.Println(names)
}
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]

切片的默认行为

在进行切片时,你可以利用它的默认行为来忽略上下界。

切片下界的默认值为 0,上界则是该切片的长度。

var a [10]int

来说,以下切片是等价的:

a[010]
a[10]
a[0]
a[]

切片的长度与容量

切片拥有 长度 和 容量。

切片的长度就是它所包含的元素个数。

切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。

切片 s 的长度和容量可通过表达式 len(s)cap(s) 来获取。

只要具有足够的容量,你就可以通过重新切片来扩展一个切片。

package main

import fmt

func main() {
s = []int{2, 3, 5, 7, 11, 13}
printSlice(s)

截取切片使其长度为 0
s = s[0]
printSlice(s)

拓展其长度
s = s[4]
printSlice(s)

舍弃前两个值
s = s[2]
printSlice(s)
}

func printSlice(s []int) {
fmt.Printf(len=%d cap=%d %vn, len(s), cap(s), s)
}

nil 切片

切片的零值是 nil

nil 切片的长度和容量为 0 且没有底层数组。

用 make 创建切片(实际常用)

切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。

make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:

a = make([]int, 5)   len(a)=5

要指定它的容量,需向 make 传入第三个参数:

b = make([]int, 0, 5)  len(b)=0, cap(b)=5

b = b[cap(b)] len(b)=5, cap(b)=5
b = b[1] len(b)=4, cap(b)=4

切片的切片

切片可包含任何类型,甚至包括其它的切片。

package main

import (
fmt
strings
)

func main() {
创建一个井字板(经典游戏)
board = [][]string{
[]string{_, _, _},
[]string{_, _, _},
[]string{_, _, _},
}

两个玩家轮流打上 X 和 O
board[0][0] = X
board[2][2] = O
board[1][2] = X
board[1][0] = O
board[0][2] = X

for i = 0; i len(board); i++ {
fmt.Printf(%sn, strings.Join(board[i], ,))
}
}
X,_,X
O,_,X
_,_,O

向切片追加元素

为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append 函数。

append 的第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。

package main

import fmt

func main() {
var s []int
printSlice(s)

添加一个空切片
s = append(s, 0)
printSlice(s)

这个切片会按需增长
s = append(s, 1)
printSlice(s)

可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)
}

func printSlice(s []int) {
fmt.Printf(len=%d cap=%d %vn, len(s), cap(s), s)
}
len=0 cap=0 []
len=1 cap=1 [0]
len=2 cap=2 [0 1]
len=5 cap=6 [0 1 2 3 4]

Range(循环取值)

for 循环的 range 形式可遍历切片或映射。

当使用 for 循环遍历切片时,每次迭代都会返回两个值。

第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本(值)。

package main

import fmt

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
for key, value = range pow {
fmt.Printf(%d = %dn, key, value)
}
}

0 = 1
1 = 2
2 = 4
3 = 8
4 = 16
5 = 32
6 = 64
7 = 128

映射 (map)

映射将键映射到值(类似python的字典)。

映射的零值为 nilnil 映射既没有键,也不能添加键。

make 函数会返回给定类型的映射,并将其初始化备用。

package main

import fmt

type Vertex struct {
Lat, Long float64
}

var m map[string]Vertex

func main() {
m = make(map[string]Vertex)
m[Bell Labs] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m[Bell Labs])
}
{40.68433 -74.39967}
package main

import fmt

type Vertex struct {
Lat, Long float64
}

var m = map[string]Vertex{
Bell Labs Vertex{
40.68433, -74.39967,
},
Google Vertex{
37.42202, -122.08408,
},
}

func main() {
fmt.Println(m)
}
map[Bell Labs{40.68433 -74.39967} Google{37.42202 -122.08408}]

在映射 m 中插入或修改元素:

m[key] = elem

获取元素:

elem = m[key]

删除元素:

delete(m, key)

通过双赋值检测某个键是否存在:

elem, ok = m[key]

keym 中,oktrue ;否则,okfalse

key 不在映射中,那么 elem 是该映射元素类型的零值。

同样的,当从映射中读取某个不存在的键时,结果是映射的元素类型的零值。

注 :若 elemok 还未声明,你可以使用短变量声明:

elem, ok = m[key]
package main

import fmt

func main() {
m = make(map[string]int)

m[Answer] = 42
fmt.Println(The value, m[Answer])

m[Answer] = 48
fmt.Println(The value, m[Answer])

delete(m, Answer)
fmt.Println(The value, m[Answer])

v, ok = m[Answer]
fmt.Println(The value, v, Present, ok)
}

The value 42
The value 48
The value 0
The value 0 Present false

函数也是值。它们可以像其它值一样传递。

函数值可以用作函数的参数或返回值。

package main

import (
fmt
math
)

func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4) 相当于递归调用了
}

func main() {
hypot = func(x, y float64) float64 {
return math.Sqrt(xx + yy)
}
fmt.Println(hypot(5, 12))

fmt.Println(compute(hypot))
fmt.Println(compute(math.Pow))
}

函数的闭包

Go 函数可以是一个闭包。

闭包是一个函数值,它引用了其函数体之外的变量。

该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。

例如,函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。

package main

import fmt

func adder() func(int) int {
sum = 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos, neg = adder(), adder()
for i = 0; i 10; i++ {
fmt.Println(
pos(i),
neg(-2i),
)
}
}

0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90

方法和接口

Go 没有类。不过你可以为结构体类型定义方法。

方法就是一类带特殊的 接收者 参数的函数。

方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。

在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。

package main

import (
fmt
math
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}

func main() {
v = Vertex{3, 4}
fmt.Println(v.Abs())
}

方法即函数

记住:方法只是个带接收者参数的函数。

现在这个 Abs 的写法就是个正常的函数,功能并没有什么变化。

package main

import (
fmt
math
)

type Vertex struct {
X, Y float64
}

func Abs(v Vertex) float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}

func main() {
v = Vertex{3, 4}
fmt.Println(Abs(v))
}

你也可以为非结构体类型声明方法。

在此例中,我们看到了一个带 Abs 方法的数值类型 MyFloat

你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。

package main

import (
fmt
math
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
if f 0 {
return float64(-f)
}
return float64(f)
}

func main() {
f = MyFloat(-math.Sqrt2)
fmt.Println(f.Abs())
}

指针接收者

你可以为指针接收者声明方法。

这意味着对于某类型 T,接收者的类型可以用 T 的文法。(此外,T 不能是像 int 这样的指针。)

例如,这里为 Vertex 定义了 Scale 方法。

指针接收者的方法可以修改接收者指向的值(就像 Scale 在这做的)。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。

试着移除第 16 行 Scale 函数声明中的 ``,观察此程序的行为如何变化。

若使用值接收者,那么 Scale 方法会对原始 Vertex 值的副本进行操作。(对于函数的其它参数也是如此。)Scale 方法必须用指针接受者来更改 main 函数中声明的 Vertex 的值。

package main

import (
fmt
math
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}

func (v Vertex) Scale(f float64) {
v.X = v.X f
v.Y = v.Y f
}

func main() {
v = Vertex{3, 4}
v.Scale(10)
fmt.Println(v.Abs())
}

方法与指针重定向

比较前两个程序,你大概会注意到带指针参数的函数必须接受一个指针:

var v Vertex
ScaleFunc(v, 5) 编译错误!
ScaleFunc(&v, 5) OK

而以指针为接收者的方法被调用时,接收者既能为值又能为指针:

var v Vertex
v.Scale(5) OK
p = &v
p.Scale(10) OK

对于语句 v.Scale(5),即便 v 是个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)

package main

import fmt

type Vertex struct {
X, Y float64
}

func (v Vertex) Scale(f float64) {
v.X = v.X f
v.Y = v.Y f
}

func ScaleFunc(v Vertex, f float64) {
v.X = v.X f
v.Y = v.Y f
}

func main() {
v = Vertex{3, 4}
v.Scale(2)
ScaleFunc(&v, 10)

p = &Vertex{4, 3}
p.Scale(3)
ScaleFunc(p, 8)

fmt.Println(v, p)
}

同样的事情也发生在相反的方向。

接受一个值作为参数的函数必须接受一个指定类型的值:

var v Vertex
fmt.Println(AbsFunc(v)) OK
fmt.Println(AbsFunc(&v)) 编译错误!

而以值为接收者的方法被调用时,接收者既能为值又能为指针:

var v Vertex
fmt.Println(v.Abs()) OK
p = &v
fmt.Println(p.Abs()) OK

这种情况下,方法调用 p.Abs() 会被解释为 (p).Abs()

package main

import (
fmt
math
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}

func AbsFunc(v Vertex) float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}

func main() {
v = Vertex{3, 4}
fmt.Println(v.Abs())
fmt.Println(AbsFunc(v))

p = &Vertex{4, 3}
fmt.Println(p.Abs())
fmt.Println(AbsFunc(p))
}

选择值或指针作为接收者

使用指针接收者的原因有二:

首先,方法能够修改其接收者指向的值。

其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。

在本例中,ScaleAbs 接收者的类型为 Vertex,即便 Abs 并不需要修改其接收者。

通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。

package main

import (
fmt
math
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Scale(f float64) {
v.X = v.X f
v.Y = v.Y f
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}

func main() {
v = &Vertex{3, 4}
fmt.Printf(Before scaling %+v, Abs %vn, v, v.Abs())
v.Scale(5)
fmt.Printf(After scaling %+v, Abs %vn, v, v.Abs())
}

接口类型 是由一组方法签名定义的集合。

接口类型的变量可以保存任何实现了这些方法的值。

注意 示例代码的 22 行存在一个错误。由于 Abs 方法只为 Vertex (指针类型)定义,因此 Vertex(值类型)并未实现 Abser

package main

import (
fmt
math
)

type Abser interface {
Abs() float64
}

func main() {
var a Abser
f = MyFloat(-math.Sqrt2)
v = Vertex{3, 4}

a = f a MyFloat 实现了 Abser
a = &v a Vertex 实现了 Abser

下面一行,v 是一个 Vertex(而不是 Vertex)
所以没有实现 Abser。
a = v

fmt.Println(a.Abs())
}

type MyFloat float64

func (f MyFloat) Abs() float64 {
if f 0 {
return float64(-f)
}
return float64(f)
}

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}

接口与隐式实现

类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有“implements”关键字。

隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。

因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。

package main

import fmt

type I interface {
M()
}

type T struct {
S string
}

此方法表示类型 T 实现了接口 I,但我们无需显式声明此事。
func (t T) M() {
fmt.Println(t.S)
}

func main() {
var i I = T{hello}
i.M()
}

接口也是值。它们可以像其它值一样传递。

接口值可以用作函数的参数或返回值。

在内部,接口值可以看做包含值和具体类型的元组:

(value, type)

接口值保存了一个具体底层类型的具体值。

接口值调用方法时会执行其底层类型的同名方法。

package main

import (
fmt
math
)

type I interface {
M()
}

type T struct {
S string
}

func (t T) M() {
fmt.Println(t.S)
}

type F float64

func (f F) M() {
fmt.Println(f)
}

func main() {
var i I

i = &T{Hello}
describe(i)
i.M()

i = F(math.Pi)
describe(i)
i.M()
}

func describe(i I) {
fmt.Printf((%v, %T)n, i, i)
}

底层值为 nil 的接口值

即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。

在一些语言中,这会触发一个空指针异常,但在 Go 中通常会写一些方法来优雅地处理它(如本例中的 M 方法)。

注意 保存了 nil 具体值的接口其自身并不为 nil。

package main

import fmt

type I interface {
M()
}

type T struct {
S string
}

func (t T) M() {
if t == nil {
fmt.Println(nil)
return
}
fmt.Println(t.S)
}

func main() {
var i I

var t T
i = t
describe(i)
i.M()

i = &T{hello}
describe(i)
i.M()
}

func describe(i I) {
fmt.Printf((%v, %T)n, i, i)
}

nil 接口值

nil 接口值既不保存值也不保存具体类型。

为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。

package main

import fmt

type I interface {
M()
}

func main() {
var i I
describe(i)
i.M()
}

func describe(i I) {
fmt.Printf((%v, %T)n, i, i)
}

指定了零个方法的接口值被称为 空接口:

interface{}

空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)

空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。

package main

import fmt

func main() {
var i interface{}
describe(i)

i = 42
describe(i)

i = hello
describe(i)
}

func describe(i interface{}) {
fmt.Printf((%v, %T)n, i, i)
}

类型断言 提供了访问接口值底层具体值的方式。

t = i.(T)

该语句断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t

i 并未保存 T 类型的值,该语句就会触发一个恐慌。

为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。

t, ok = i.(T)

i 保存了一个 T,那么 t 将会是其底层值,而 oktrue

否则,ok 将为 falset 将为 T 类型的零值,程序并不会产生恐慌。

请注意这种语法和读取一个映射时的相同之处。

package main

import fmt

func main() {
var i interface{} = hello

s = i.(string)
fmt.Println(s)

s, ok = i.(string)
fmt.Println(s, ok)

f, ok = i.(float64)
fmt.Println(f, ok)

f = i.(float64) 报错(panic)
fmt.Println(f)
}

类型选择 是一种按顺序从几个类型断言中选择分支的结构。

类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。

switch v = i.(type) {
case T
v 的类型为 T
case S
v 的类型为 S
default
没有匹配,v 与 i 的类型相同
}

类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type

此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 TS 的情况下,变量 v 会分别按 TS 类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 vi 的接口类型和值相同。

package main

import fmt

func do(i interface{}) {
switch v = i.(type) {
case int
fmt.Printf(Twice %v is %vn, v, v2)
case string
fmt.Printf(%q is %v bytes longn, v, len(v))
default
fmt.Printf(I don't know about type %T!n, v)
}
}

func main() {
do(21)
do(hello)
do(true)
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK