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" ) }
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" ) }
指针
变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。
当使用&
操作符对变量进行取地址操作并得到变量的指针后,可以对指针使用*
操作符,也就是指针取值
取地址操作符&
和取值操作符*
是一对互补操作符,**&
取出地址,*
根据地址取出地址指向的值。**
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
对变量进行取地址操作使用&
操作符,可以获得这个变量的指针变量。
指针变量的值是指针地址。
对指针变量进行取值操作使用*
操作符,可以获得指针变量指向的原变量的值。
其实归纳起来,*
操作符的根本意义就是操作指针指向的变量 。
当操作在右值时,就是取指向变量的值,
当操作在左值时,就是将值设置给指向的变量。
结构体字段与结构体指针
结构体字段可以使用点号
来访问。
结构体字段可以使用结构体指针
来访问。
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) fmt.Println(v.X,v.Y) p := &v p.X = 4 p.Y = 5 fmt.Println(v) fmt.Println(v.X,v.Y) }
函数别名 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()) }
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()) }
值接收者和指针接收者在方法调用的区别
在调用方法的时候,值类型既可以调用值接收者
的方法,也可以调用指针接收者
的方法;指针类型既可以调用指针接收者
的方法,也可以调用值接收者
的方法。
也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。
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 := Person{age: 18 } fmt.Println(qcrao.howOld()) qcrao.growUp() fmt.Println(qcrao.howOld()) stefno := &Person{age: 100 } fmt.Println(stefno.howOld()) stefno.growUp() fmt.Println(stefno.howOld()) }
调用了 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() }
隐式接口 在动态语言 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) f(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) f(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) t = Teacher() say_hello(t) e = Engineer() say_hello(e)
上面代码是典型的鸭子类型. 不管是谁 , 要想成功调用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 {}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) } 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 可以在没有明确的锁或竞态变量的情况下进行同步。