GOLANG sucks

程序入口

go项目的启动入口严格为main包下的main方法

image-20230711150026476

导出名

理解为java的public和private权限修饰符

在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。例如,Pizza 就是个已导出名,Pi 也同样,它导出自 math 包。

pizzapi 并未以大写字母开头,所以它们是未导出的。

在导入一个包时,你只能引用其中已导出的名字。任何“未导出”的名字在该包外均无法访问。

image-20230711145720177

如hello.go的SayHello函数,当它被命名为sayHello时无法被包外调用

函数

  • 传参格式为值在前,类型在后; 连续相同类型可以省略类型声明

    func test(x int, y int) (int, int){} 等价于 func test(x, y int) (int, int){}

  • 可以定义返回值,同样当连续相同类型可以省略类型声明

    func test(x int, y int) (m int, n int){} 等价于 func test(x int, y int) (m, n int){}

    • 当定义返回值时,没有参数的 return 语句将返回已命名的返回值。也就是 直接 返回。

      func swap(x, y int) (m, n int) {
      m = y
      n = x
      return // return后没有参数,即返回m和n
      }

基本类型

Go 的基本类型有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool

string

int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
// 表示一个 Unicode 码点

float32 float64

complex64 complex128

本例展示了几种类型的变量。 同导入语句一样,变量声明也可以“分组”成一个语法块。

int, uintuintptr 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。 当你需要一个整数值时应使用 int 类型,除非你有特殊的理由使用固定大小或无符号的整数类型。

零值

没有明确初始值的变量声明会被赋予它们的 零值

零值是:

  • 数值类型为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)。

变量声明

var 语句用于声明一个变量列表,跟函数的参数列表一样,类型在最后。

var 语句可以出现在包或函数级别。

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

var c, python, java bool

func main() {
var i int
fmt.Println(i, c, python, java)
}

//output: 0 false false false

声明的时候可以包含初始值

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

var i, j int = 1, 2 //此处int可以省略 因为已经声明的变量是明确的

func main() {
var c, python, java = true, false, "no!"
fmt.Println(i, j, c, python, java)
}

//output: 1 2 true false no!

短变量声明

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。

在声明一个变量而不指定其类型时(即使用不带类型的 := 语法或 var = 表达式语法),变量的类型由右值推导得出。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
var i, j int = 1, 2
k := 3
c, python, java := true, false, "no!"

fmt.Println(i, j, k, c, python, java)
}

//output: 1 2 3 true false no!

类型转换

表达式 T(v) 将值 v 转换为类型 T

一些关于数值的转换:

1
2
3
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

或者,更加简单的形式:

1
2
3
i := 42
f := float64(i)
u := uint(f)

与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换。

循环语句

go的循环只有for语句

注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。

1
2
3
for i := 0; i < 10; i++ {
//do something
}

选择语句

if

  • Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

  • if 语句可以在条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内。

    1
    2
    3
    if i := 0; i < 10086{
    //do something
    }

switch

一样没有小括号,一样可以先执行一个简单语句

switch 是编写一连串 if - else 语句的简便方法。它运行第一个值等于条件表达式的 case 语句。

  • Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。
  • switch 的 case 无需为常量,且取值不必为整数。
  • switch 的 case 语句从上到下顺次执行,直到匹配成功时停止
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}
//output: Go runs on Linux.

没有条件的 switch 同 switch true 一样。

这种形式能将一长串 if-then-else 写得更加清晰。

1
2
3
4
5
6
7
8
9
10
11
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}

defer

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func main() {
fmt.Println("counting")

for i := 0; i < 10; i++ {
defer fmt.Println(i)
}

fmt.Println("done")
}
//output:
counting
done
9
8
7
6
5
4
3
2
1
0

指针

Go 拥有指针。指针保存了值的内存地址。

类型 *T 是指向 T 类型值的指针。其零值为 nil

1
var p *int

& 操作符会生成一个指向其操作数的指针。

1
2
i := 42
p = &i

* 操作符表示指针指向的底层值。

1
2
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i

这也就是通常所说的“间接引用”或“重定向”。

与 C 不同,Go 没有指针运算。

结构体

1
2
3
4
5
6
7
8
9
10
11
//定义
type Vertex struct {
X int
Y int
}
//实例化
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予(零值)
v3 = Vertex{} // X:0 Y:0
//引用
v1.X = 4

结构体字段可以通过结构体指针来访问。

如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X 就可以。

数组

1
2
3
4
//定义
var a [10]int

primes := [6]int{2, 3, 5, 7, 11, 13}

切片

每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。

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

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

1
2
3
4
5
6
a[low : high] //左闭右开

//如有int数组primes
primes := [6]int{2, 3, 5, 7, 11, 13}
//s即为primes数组切片
var s []int = primes[1:4]

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

对于数组

1
var a [10]int

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

1
2
3
4
a[0:10]
a[:10]
a[0:]
a[:]

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

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

切片拥有 长度容量

  • 切片的长度就是它所包含的元素个数。
  • 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
  • 切片 s 的长度和容量可通过表达式 len(s)cap(s) 来获取。
  • 可以通过重新切片来扩展一个切片(重新对切片赋值),给它提供足够的容量。

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

向切片追加元素

为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append 函数。内建函数的文档对此函数有详细的介绍。

1
func append(s []T, vs ...T) []T

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

append 的结果是一个包含原切片所有元素加上新添加元素的切片。

s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。

1
2
3
var s []int //len=0 cap=0 []
s = append(s, 1) //len=1 cap=1 [1]
s = append(s, 2, 3, 4) //len=4 cap=4 [1 2 3 4]

数组vs切片

1
2
3
4
5
[3]bool{true, true, false}

下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:

[]bool{true, true, false}

动态数组(用 make 创建切片)

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

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

1
a := make([]int, 5)  // len(a)=5

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

1
2
3
4
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

映射

1
var m map[string]Vertex

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type Vertex struct {
Lat, Long float64
}

var m map[string]Vertex
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}

//等价于
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
}
//等价于(若顶级类型只是一个类型名,你可以在文法的元素中省略它)
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
}

删除元素:

1
delete(m, key)

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

1
elem, ok = m[key]

keym 中,oktrue ;否则,okfalse

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

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

range

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

当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。

1
2
3
4
5
6
7
8
var pow = [8]int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}

//若不需要某个值,可以将下标或值赋予 _ 来忽略它。
for i, _ := range pow
for _, v := range pow

闭包

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

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

Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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(-2*i),
)
}
}

//可以理解为将一个方法栈赋值给一个变量
output:
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90

方法

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

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

1
2
3
4
5
6
7
8
9
type Vertex struct {
X, Y float64
}

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

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

方法的一个非常有用的特性是能够将它们链接在一起,同时仍保持代码的清洁。

1
2
3
4
p := &Person{}

p = p.withName("John").withAge(21) //方法
p = withName(withAge(p, 18), "John") //函数

Tips:

  • 接收者的类型定义和方法声明必须在同一包内
  • 不能为内建类型(如int)声明方法。

接口

由一组方法签名定义的集合。

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

    • 隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。
    • 因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。
  • 接口也是值。它们可以像其它值一样传递。

    • 接口值可以用作函数的参数或返回值。
  • 即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// I接口定义
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()
}

空接口

指定了零个方法的接口值

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

空接口被用来处理未知类型的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
	var i interface{}
describe(i)

i = 42
describe(i)

i = "hello"
describe(i)
}

func describe(i interface{}) {
fmt.Printf("(%v, %T)\n", i, i)
}
//output:
(<nil>, <nil>)
(42, int)
(hello, string)

类型断言

提供了访问接口值底层具体值的方式

1
t := i.(T)

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

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

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

1
t, ok := i.(T)

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
}
//output:
hello
hello true
0 false
panic: interface conversion: interface {} is string, not float64

类型选择

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

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

1
2
3
4
5
6
7
8
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 的接口类型和值相同。