go_tour.md

变量定义

使用var关键字

  1. 带类型
  2. 可以一次定义多个变量, 如果变量类型相同,只需要写最后一个的变量类型
  3. 在函数中可以使用 := 短声明, 函数体外不可使用

基本类型

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

变量初始化时的值为 ‘零值’

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

类型转换

使用 type(value)的方式转换

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)

不同于js, 在GO中, 不允许隐式转换。会编译不通过。

虽然没有隐式转换,但是有类型推导

我们定义一个变量,但是不指定其类型时, 变量进行右查询, 根据右查询的结果决定变量类型.

1
2
var i int
j := i // j 也是一个 int

但当右边包含的是变量可能是int, float或者complex128的时候,取决于精度

1
2
3
i := 42           // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128

常量

常量使用const关键字.但是不可以使用简洁语法(:=)

一个没有显示指定类型的常量由上下文环境决定类型.

for循环

1
2
3
4
sum := 0
for i := 0; i < 10; i++ {
sum += i
}

和js不一样, 不需要有括号, 但是{}, 花括号是必须的.

我们可以把前置语句和后置语句置空.做一个while循环

1
2
3
4
sum := 1
for sum < 1000 {
sum += sum
}

条件语句 if

没有括号,但是花括号是必须的.

1
2
3
   if x < 0 {
return sqrt(-x) + "i"
}

在if语句中,可以在条件语句之前执行一段代码:

1
2
3
if v := math.Pow(x, n); v < lim {
return v
}

if-else 在if中的便捷代码里的变量,我们可以在else语句延用,但是,出了条件语句(if, else语句中), 那就脱离了范围,不可以使用了.换句话说,变量作用域在if, else语句体内

swich

和平常的swich没有什么区别,就是条件语句特性就是可以在判定条件前加一个简洁语句.

1
2
3
4
5
6
7
8
9
10
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.", os)
}

没有条件的swich可以用来做if-else的多分支语句。

1
2
3
4
5
6
7
8
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}

defer语句 延迟执行

defer 语句会延迟函数的执行直到上层函数返回。

延迟调用的参数会立刻生成,但是在上层函数返回前函数都不会被调用。

defer执行顺序,所有的defer都会被压入一个栈, 执行的时候会按照先进后执行的顺序.


指针

go中存在指针, 指针存的是变量的内存地址.

定义的方式是* type, type是值的类型, 也就是这个地址内存的是什么类型的值。初始值是nil

1
var p *int

& 符号会生成一个指向其作用对象的指针。也就是生成一个引用值, 新建一个区域, 存入指向对象的地址。

1
2
3
4
i := 42
p = &i
// 此时, 如果直接println(p) -> 读出来是内存地址 0xc000056058 (类似这种)
// 而使用println(*p)那么就表面,我通过p,去读取内存地址为0xc000056058的值, 此时为42.

*符号表示指针指向的底层的值。我觉得指针操作带来的便利是, 性能, 我们并不需要每个值都copy一份.就像js里的值传递和地址传递.通过指针, 我们可以很大程度的优化我们的代码执行效率.

这也就是间接引用和直接引用的一个区别.间接引用通过指针和 * 来直接读取到被引用的值.

go中么有指针运算(指针运算是什么?)

结构体

怎么说,很像js的对象, 但是又有点不像

定义方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type structName struct {
// 结构体
}

// 例子
package main

import "fmt"

type Vertex struct {
X int
Y int
}

func main() {
fmt.Println(Vertex{1, 2})
}

结构体访问方式

使用.运算符访问

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

import "fmt"

type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
}

结构体指针

结构体可以通过指针来访问, 这意味着, 我们可以拥有类似原型链的能力了

这里要说的是, 当我们print(p)的时候, 得出的并不是内存的真实地址, 而是 &{1, 2} 的字符串, 但是此时, p和v是有相同的功能的, 由于p是引用,在P上的操作会直接反应到v上.这像不像js的地址传递.

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

import "fmt"

type Vertex struct {
X int
Y int
}

func main() {
v := Vertex{1, 2}
p := &v
p.X = 1e9
fmt.Println(v)
}

结构体文法

  1. 结构体文法表示通过结构体字段的值作为列表来新分配一个结构体。这里的概念类似于js的结构赋值, 此时有顺序的概念
    1
    2
    3
    4
    5
    type Vertex struct {
    X, Y int
    }

    var v1 = Vertex{1, 2} // x:1 y:2
  2. 使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)
    1
    2
    3
    4
    5
    6
    type Vertex struct {
    X, Y int
    }

    var v2 = Vertex{x: 2, y: 1} // x:1 y:2
    var v3 = Vertex{y: 2, x: 1} // x:1 y:2
  3. 特殊的前缀 & 返回一个指向结构体的指针。使用结构体的指针和使用结构体本身没多大区别, 这一点要牢记.
    1
    2
    3
    4
    5
    type Vertex struct {
    X, Y int
    }

    var v4 = &Vertex{1, 2}

数组

定义方式

1
2
3
var arrayName [length]Type // 意思是 1. 我要定义一个名字为arrayName的数组 2.它的长度为length 3.里面的元素是Type类型的

var a [18]int

注意,在go中, 数组的长度是不可变的.但是go提供了一种机制解决这个问题.

slice

定义一个slice, 这个类似于js中的temp数组.临时数组.

1
2
[]Type // 类型为Type的slice
p := []int{2, 3, 4, 5, 6, 7}

我们可以访问的slice的长度信息通过len(p)

slice切片处理

表达式

1
s[low:high] // 对数组s, 切片从low到high-1

slice数组产生方式二 make()

1
2
3
4
5

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

// 除了指定长度, 我们也可以指定数组容量, 前面说过了, 在go中,数组的长度是固定的, 超出就会报错
b := make([]int, 0, 5) // length(b) -> 0 cap(b) -> 5

slice的初始值 nil

一个slice的初始值为nil, 特征是长度和容量都为0

1
2
3
4
5
var z []int
fmt.Println(z, len(z), cap(z))
if z == nil {
fmt.Println("nil!")
}

往数组中添加元素 append

go内建函数提供往数组尾部插入元素的方法

1
2
3
func append(s []T, vs ...T) []T
// 使用实例
a = append(a, 2, 3, 4)

参数解读
s为类型为T的数组
其余类型为T的数组元素都会添加到slice数组中
输出是, 类型为T的数组S加上参数T的类型归集为一个slice
如果数组s太小, 那么go会为其分配一个更大的数组.返回的slice会指向这个新数组

Range

for循环的range格式可以遍历slice或者进行map循环

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

import "fmt"

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

func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}

如果想用range循环又不需要index, 或者value, 或者两样都不需要, 那么你可以用_符号忽略, 并且仍然使用循环体.

Map键值对

map使用前必须使用make创建

没有使用make的map值为nil的map是空的,并且不能赋值

使用make并没有赋值的时候,会按类型初始化.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义一个键值对
type Vertex struct {
a, b int
}

var m map[string]Vertex

func main() {
m = make(map[string]Vertex)
m["yea"] = Vertex{
40, 20
}
fmt.println(m["yeah"])
}

Map语法:类似于结构体, 但是key值必须有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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)
}

如果定义value值的类型和顶层的类型一致, 那么可以省略value值的类型名称(Vertex)

1
2
3
4
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}

操作map

  1. 增加key:value
    1
    m[key] = element
  2. 获取元素
    1
    element = m[key]
  3. 删除元素
    1
    delete(m, key)
  4. 检测某个元素在不在
    1
    elem, ok = m[key] // 如果key在m中, 那么ok为true, 否则ok为false, 并且elem为map元素m的零值
    同时, 如果我们从m中获取不存在的key那么我们会得到m的零值

在go中,函数也是值, 这一点和js很像了, 意味着我可以使用函数定义式和函数表达式.

同样,在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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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(-2*i),
)
}
}
```
那有闭包就可以有FP了.有意思了哦.

我居然写不出go的斐波那契数列.失落.
## 方法
go中没有类, 但是在结构体中可以定义方法
```go
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

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

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

方法: 给非结构体声名方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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())
}

以上,定义了一个类型, 这个类型继承于float64, 然后呢, 我给这个类型增加了方法Abs, 此时, 该方法和平常结构体的方法调用方式一致.

指针方法与结构体方法的区别

在我理解, 定义一个结构体方法, 该方法内, 我去修改结构体的变量, 此时, 并不会反应到结构体挂载的变量上.但是我用了指针方法, 那么此时, 相当于我可以取到当前实例,的结构体本身, 继而对结构的变量进行修改.

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
package main

import (
"fmt"
// "math"
)

type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
v.X = v.X * 2
v.Y = v.Y * 2
result := v.X * v.Y
return result
}

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

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

指针函数

入参指针的函数, 入参必须为指针.

而在指针结构体上定义的函数, 调用者可为结构体的指针类型, 也可以为结构体本身.

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
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)
}

同样的道理, 如果你定义了一个函数, 这个函数的入参规定为值类型, 而非指针类型,那么传入指针会导致程序在编译的时候抛出异常。

而在指针结构体上定义的函数, 调用者可为结构体的指针类型, 也可以为结构体本身.

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
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

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

func AbsFunc(v Vertex) float64 {
return math.Sqrt(v.X*v.X + v.Y*v.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))
}

说了那么多指针的优点, 就是想告诉你, 多用指针类结构体定义的函数.

  1. 在指针结构体上定义的函数, 可以修改实例的值(通过指针)。
  2. 避免在每次调用该方法的时候 ,复制使用到的值(优化性能)

原则: 所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。

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
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.X*v.X + v.Y*v.Y)
}

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

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

怎么解释捏, 就是, 这是一个代理, 这个代理上, 有被代理对象的所有方法.我们可以通过这个代理对象去调用被代理对象的方法.

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
31
32
33
34
35
36
37
38
39
40
41
42
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.X*v.X + v.Y*v.Y)
}

接口与隐式实现

通过上面的代码示例我们可以看出来, 在go中,接口直接通过interface类去实现.

假设我定义了一个接口F有方法Z

我定义了一个结构体W, 在这个结构体实现了Z

那么此时, 我声明一个接口类F, 然后, 给它赋W的实例.

此时, F就可以直接调用方法Z.形成了一个代理.

但是, F不可以访问Z上的自定义属性, 只能访问到Z与F共同定义的方法.也就是接口传递, 不传递属性.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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()
}

接口类型

接口值包含两个东西1. 被代理对象 2. 被代理的类型.

接口本身也是值, 可以像普通的类型进行传递.

从下面的实例代码我们可以看出, 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
31
32
33
34
35
36
37
38
39
40
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 的接口值