GO指南

https://go-tour-zh.appspot.com/basics/1

基本类型

  • bool
  • string
  • int int8 int16 int32 int64
  • uint uint8 uint16 uint32 uint64 uintptr
  • byte : uint8 的别名
  • rune : int32 的别名 , 代表一个Unicode码
  • float32 float64
  • complex64 complex128

没有条件的 switch

  • 没有条件的 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
func main() {
num := 2
defer fmt.Println(num)
fmt.Println("hello")
}
//hello
//2

defer 栈

  • defer的函数是以的形式存储的
  • 延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。
1
2
3
4
5
6
7
8
9
10
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Print(i)
}
fmt.Println("done")
}
// counting
// done
// 9876543210

指针

  • 变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。
  • 当使用&操作符对变量进行取地址操作并得到变量的指针后,可以对指针使用*操作符,也就是指针取值
  • 取地址操作符&和取值操作符*是一对互补操作符,**&取出地址,*根据地址取出地址指向的值。**

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址操作使用&操作符,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值操作使用*操作符,可以获得指针变量指向的原变量的值。
  • *操作符作为右值时,意义是取指针的值,

  • 作为左值时,也就是放在赋值操作符的左边时,表示 a 指针指向的变量。

其实归纳起来,*操作符的根本意义就是操作指针指向的变量

  1. 当操作在右值时,就是取指向变量的值,
  2. 当操作在左值时,就是将值设置给指向的变量。
  • 类型 *T 是指向类型 T 的值的指针。其零值是 nil

  • & 符号会生成一个指向其作用对象的指针。

结构体字段与结构体指针

  • 结构体字段可以使用点号来访问。
  • 结构体字段可以使用结构体指针来访问。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type Vertex struct {
X int
Y int
}

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

p := &v
p.X = 4
p.Y = 5
fmt.Println(v) // {4 5}
fmt.Println(v.X,v.Y) // 4 5
}

函数别名

1
2
3
4
5
6
7
func main() {
hypot := func(x, y float64) float64 {
return math.Sqrt(x*x + y*y)
}

fmt.Println(hypot(3, 4))
}

闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func adder() func(x int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}

func main() {
pos := adder()
for i := 0; i < 10; i++ {
fmt.Println(pos(i))
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func fibonacci() func() int {
slow,fast := 0,1
return func() int {
slow ,fast = fast,slow+fast
return slow
}
}

func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}

方法

  • Go 没有类。然而,仍然可以在结构体类型上定义方法。
  • 方法接收者 出现在 func 关键字和方法名之间的参数中。
1
2
3
4
5
6
7
8
9
10
11
12
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
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())
}

不能对来自其他包的类型或基础类型定义方法。

接收者为指针的方法

有两个原因需要使用指针接收者。

  • 避免在每个方法调用中拷贝值(如果值类型是大的结构体的话会更有效率)。
  • 方法可以修改接收者指向的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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}
v.Scale(5)
fmt.Println(v, v.Abs())
}

GO的值传递和引用传递

  • 在python中 , 如果是可变对象 , 是引用传递 ; 如果是不可变对象 , 是值传递
  • 默认情况下,Go 不管是什么对象 , 使用的都是值传递。
传递类型 描述
值传递 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递 引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
  • 在Go语言中,默认是按值传递.
  • 当一个变量当作参数传递的时候,会创建变量的副本, 然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的.
  • 当变量当做指针被传递的时候,新的指针被创建,它指向变量同样的内存地址, 所以你可以将这个指针看成原始变量指针的副本.

func Func(t *Type) {} VS func Func(t Type) {}

入参是指针,参数值可以在函数内部被修改.

func Func()(t *Type) {} VS func Func()(t Type) {}

返回值类型不同之处在于取值的方式,指针类型需要使用 * 号读取数据. 其次返回值指针判断空值更加容易简洁,t != nil.

func (t *Type) Method() {} VS func (t Type) Method() {}

  • 如果要在方法中更改receiver的状态,操纵receiver的值,请使用指针receiver.
  • 使用值receiver是不可能的, 它按值复制.对值receiver的任何修改都是该值receiver副本的本地修改.

值receiver在原始类型值的副本上运行. 这意味着涉及成本,特别是如果结构非常大,并且接收的指针更有效. 如果您不需要编辑receiver值,请使用值receiver.

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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}
v.Scale(5)
fmt.Println(v, v.Abs()) // &{15 20} 25
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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}
v.Scale(5)
fmt.Println(v, v.Abs()) // {3 4} 5
}

值接收者和指针接收者在方法调用的区别

  • 在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。
  • 也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。
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
type Person struct {
age int
}

func (p Person) howOld() int {
return p.age
}

func (p *Person) growUp() {
p.age += 1
}

func main() {
// qcrao 是值类型
qcrao := Person{age: 18}

// 值类型 调用 接收者是值类型 的方法
fmt.Println(qcrao.howOld()) // 18

// 值类型 调用 接收者是指针类型 的方法
qcrao.growUp()
fmt.Println(qcrao.howOld()) // 19

// stefno 是指针类型
stefno := &Person{age: 100} // 100

// 指针类型 调用 接收者是值类型 的方法
fmt.Println(stefno.howOld())

// 指针类型 调用 接收者是指针类型 的方法
stefno.growUp()
fmt.Println(stefno.howOld()) //101
}

调用了 growUp 函数后,不管调用者是值类型还是指针类型,它的 Age 值都改变了。

实际上,当类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作,这里面实际上通过语法糖起作用的。用一个表格来呈现:

- 值接收者 指针接收者
值类型调用者 方法会使用调用者的一个副本,类似于“传值” 使用值的引用来调用方法,上例中,qcrao.growUp() 实际上是 (&qcrao).growUp()
指针类型调用者 指针被解引用为值,上例中,stefno.howOld() 实际上是 (*stefno).howOld() 实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针

值接收者和指针接收者

不管接收者类型是值类型还是指针类型,都可以通过值类型或指针类型调用,这里面实际上通过语法糖起作用的。

如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。

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
type coder interface {
code()
debug()
}

type Gopher struct {
language string
}

func (p Gopher) code() {
fmt.Printf("I am coding %s language\n", p.language)
}

func (p *Gopher) debug() {
fmt.Printf("I am debuging %s language\n", p.language)
}

func main() {
var c coder = &Gopher{"Go"}
c.code()
c.debug()

var c coder = Gopher{"Go"}
c.code()
c.debug() // error
}

隐式接口

在动态语言 python 中,定义一个这样的函数:

1
2
def hello_world(coder):
coder.say_hello()
  • 当调用此函数的时候,可以传入任意类型,只要它实现了 say_hello() 函数就可以。如果没有实现,运行过程中会出现错误。
  • 而在静态语言如 Java, C++ 中,必须要显式地声明实现了某个接口,之后,才能用在任何需要这个接口的地方。如果你在程序中调用 hello_world 函数,却传入了一个根本就没有实现 say_hello() 的类型,那在编译阶段就不会通过。这也是静态语言比动态语言更安全的原因。
  • 动态语言和静态语言的差别在此就有所体现。静态语言在编译期间就能发现类型不匹配的错误,不像动态语言,必须要运行到那一行代码才会报错。
  • Go 语言作为一门现代静态语言,是有后发优势的。它引入了动态语言的便利,同时又会进行静态语言的类型检查,写起来是非常 Happy 的。
  • 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
type IGreeting interface {
sayHello()
}

type Go struct {}
type PHP struct {}

func (g Go) sayHello() {
fmt.Println("Hi, I am GO!")
}
func (p PHP) sayHello() {
fmt.Println("Hi, I am PHP!")
}

func f(i IGreeting) {
i.sayHello()
}

func main() {
golang := Go{}
php := PHP{}
f(golang) // Hi, I am GO!
f(php) // Hi, I am PHP!
}
  • 在 main 函数中,调用调用 f() 函数时,传入了 golang, php 对象,它们并没有显式地声明实现了 IGreeting 类型,只是实现了接口所规定的 sayHello() 函数。
  • 实际上,编译器在调用 f() 函数时,会隐式地将 golang, php 对象转换成 IGreeting 类型,这也是静态语言的类型检查功能。

因为GO函数的参数必须传入类型 , 通过GO独有的接口 , 我们就可以通过定义定义f函数来进行隐式的类型转换

1
2
3
4
5
6
7
8
func f(i IGreeting) {
i.sayHello()
}

golang := Go{}
php := PHP{}
f(golang) // Hi, I am GO!
f(php) // Hi, I am PHP!

Py , Java 和 Go的接口对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def say_hello(instance):
return instance.say_hello()

class Student:
def say_hello(self):
print('hello student')

class Teacher:
def say_hello(self):
print('hello tearcher')

class Engineer:
pass


s = Student()
say_hello(s) # hello student

t = Teacher()
say_hello(t) # hello student

e = Engineer()
say_hello(e) # 因为没有say_hello() , 执行到这里的时候报错
  • 上面代码是典型的鸭子类型. 不管是谁 , 要想成功调用say_hello()函数 , 就要传入一个实现了say_hello方法的类实例
  • 鸭子类型是动态语言的典型 , 也就是说逐行执行代码 , 有错就抛出 , 直到23行之前代码都没有错 , 所以say_hello(s)和say_hello(t)都能成功执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Animal {
public void eat();
public void travel();
}

public class MammalInt implements Animal{
public void eat(){
System.out.println("Mammal eats");
}

public void travel(){
System.out.println("Mammal travels");
}

public int noOfLegs(){
return 0;
}

public static void main(String args[]){
MammalInt m = new MammalInt();
m.eat();
m.travel();
}
}
  • 定义了一个Animal接口 , 这个接口必须实现eat方法和travel方法
  • 定义了一个哺乳动物(MammalInt)类 , 这个类显式继承了Animal接口( implements Animal) , 并且在接下来具体实现了eat方法和travel方法
  • 显式继承接口 是静态语言的典型 . 程序在执行前会进行编译 , 编译过程中就会检查MammalInt类是否具体实现了eat方法和travel方法 , 如果没有则会报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Animal interface{
eat()
}

type MammalInt struct{}

// MammalInt具体实现Animal接口的部分方法
func (m MammalInt) eat(){
fmt.Println("MammalInt eating")
}

func f(a Animal) {
a.eat()
}

func main() {
m := MammalInt{}
f(m)
}
  • 在 main 函数中,调用 f() 函数时,传入了 MammalInt 对象,它们并没有显式地声明实现了 Animal 类型(并没有 implements Animal),只是实现了接口所规定的 eat() 函数。
  • 实际上,编译器在调用 f() 函数时,会隐式地将 MammalInt 对象转换成 Animal 类型,这也是静态语言的类型检查功能。

综上所述 :

  • 动态语言 :
    • 好处 : 动态绑定 , 不需要实现接口 , 容易扩展(新定义的类只要实现say_hello()方法即可)
    • 劣处 : 如果没有实现,运行过程中会出现错误 , 不安全。
  • 静态语言 :
    • 好处 : 显式继承接口 , 编译时进行检测 , 如果类没有具体实现接口方法 , 就会报错 , 因而安全
    • 劣处 : 代码量多 , 复杂
  • GO : 综合静态语言 和 动态语言的好处。
    • 隐式继承接口 , 只要实现了接口所规定的函数即可。可拓展
    • 编译时进行检测有没有具体实现接口方法, 因而安全
  • 鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它当前方法和属性的集合决定。
  • Go 作为一种静态语言,通过接口实现了 鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。

常用接口

Stringer接口

一个普遍存在的接口是 fmt 包中定义的 Stringer

1
2
3
type Stringer interface {
String() string
}

类似于python中的__str____repr__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type Person struct {
Name string
Age int
}

func (p Person) String() string {
return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
a := Person{"Arthur Dent", 42}
z := Person{"Zaphod Beeblebrox", 9001}
fmt.Println(a, z)
}

error接口

Go 程序使用 error 值来表示错误状态。

fmt.Stringer 类似,error 类型是一个内建接口:

1
2
3
type error interface {
Error() string
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type MyError struct {
When time.Time
What string
}

func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}

// 函数抛出error , 就会调用Error()方法
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}

func main() {
if err := run(); err != nil {
fmt.Println(err)
}
}

Reader 接口

  • io 包指定了 io.Reader 接口, 它表示从数据流结尾读取。
  • Go 标准库包含了这个接口的许多实现, 包括文件、网络连接、压缩、加密等等。

io.Reader 接口有一个 Read 方法:

1
func (T) Read(b []byte) (n int, err error)

Read 用数据填充指定的字节 slice,并且返回填充的字节数和错误信息。 在遇到数据流结尾时,返回 io.EOF 错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import (
"fmt"
"io"
"strings"
)

func main() {
r := strings.NewReader("Hello, Reader!")

b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}

goroutine

1
2
3
4
5
6
7
8
9
10
11
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
go say("world")
say("hello")
}

channel

  • 默认情况下,在另一端准备好之前,发送和接收都会阻塞
  • 这使得 goroutine 可以在没有明确的锁或竞态变量的情况下进行同步。