Go tour

本篇内容多数来自于Go语言之旅中文教程。

基础

包、变量、函数

包、导入

每个go程序都被包括在一个包中。这个包在程序始以package packname确定。程序从main包开始运行。
通过import语句可以导入使用其他包中的内容,比如,在下面的程序中导入了fmt以及math/rand两个包。

package main
import(
  "fmt"
  "math/rand"
)
//也可以使用单个导入
//import "fmt"
//import "math/rand"
func main(){
  fmt.Println("my favorite number is ",rand.Intn(10))
}

约定:包名与导入路径的最后一个元素一致,比如,包math/rand中的源码均以package rand开头。

可以看到,go语言不使用分号,包中的引用需要指定包名。

导出名

go中,可以在一个包中导入另一个包,并且使用这个被导入包中的变量、方法。不过,在被导入包中,存在一些变量、方法不能被访问,这是因为它们没有被导出。没有被导出的名字在包外不能被访问。
如果一个名字以大写字母开头,它就是已导出的。比如math包中的变量Pi,它就是一个被导出的变量:

func main() {
	fmt.Println(math.Pi)
}

运行结果:3.141592653589793
如果使用math.pi,则会报错:

# command-line-arguments
.\compile5.go:9:14: cannot refer to unexported name math.pi
.\compile5.go:9:14: undefined: math.pi

变量

声明

使用var语句声明一个变量列表:

var c, python, java bool//bool类型的三个变量c\python\java

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

初始化
如果类型在声明的时候就已经被初始化,可以在声明语句中省略类型声明,直接从初始值中获取变量类型。(有点类似于C++中的auto

var i,j int =1,2//初始化
var c,python,java=true,false,"no!"//省略类型声明的初始化

在声明的时候没有被初始化的变量会被默认赋初值为零值,数值的零值为0,布尔类型为false,字符串为""

短变量
函数中,简洁赋值语句:=可以替代var声明,而在函数外的每个语句都必须以关键字开始(varfunc等等),不可以使用:=结构。

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

函数

参数

函数可以没有参数或者接受多个参数,以func标识,注意类型在变量名之后。定义如下:

func add(x int,y int)int{
  return x+y
}

参照这里了解为何类型放变量名之后

如果两个或多个函数的已命名形参类型相同时,除了最后一个类型,其他变量的类型可以省略,例如上例可以改为:

func add (x,y int)int{
  return x+y
}

返回值
函数可以返回多个返回值:

func swap(x,y string)(string,string){
  return y,x
}

func main() {
	a, b := swap("hello", "world")
	fmt.Println(a, b)
}

除此之外,可以直接为返回值命名,这样,在函数体中使用指定名字的变量,该变量会被自动直接返回,而无需在return语句中显式指定。
直接返回参数列表类似于函数参数列表。

func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return
}

直接返回语句应该仅仅用于短函数,如果用在长函数中会影响可读性。

基本类型

go的基本类型有:
其中,intuintuintptr的宽度随着系统变化而变化,在32位系统上为32bits,在64位系统上是64bits。

bool
string
int    int8    int16   int32    int64
uint   uint8  uint16  uint32   uint64 uintptr

byte // uint8 的别名

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

float32  float64

complex64   complex128

类型转换与类型推导

使用表达式Type(v)v转为Type类型。

使用右值类型来推导左值的类型。

var i int
j:=i
g:=0.8+0.6i

常量

常量使用const关键字声明。类型可以是字符、字符串、布尔值或数值,不能使用:=短变量,必须使用=

const pi=3.14
const world="the world"
const istrue=true

数值常量是高精度的,一个未指定类型的常量使用上下文来确定类型。

流程控制语句

循环:for语句

go只支持for循环,基本的for循环由三部分组成,使用分号分开:初始化语句条件表达式后置语句,执行方式就和C++一样。
一般来说,初始化语句通常是短变量声明,同C++一样,作用域仅仅在for语句的作用域中。

for i:=0;i<10;i++ {//没有小括号,大括号必须存在
  fmt.Println(i)
}

同C++一样,初始化语句后置语句都是可选的,最简情况下就是一个类似C++的while语句了,此时只保留条件表达式,两侧的分号也被省略。

for ;i<10; {
  fmt.Println(i)
}

for i<10 {
  fmt.Println(i)
}

条件表达式也可以不存在,此时就是一个空语句的无限循环了。

for {

}

分支:if语句-else

if语句也可以在条件表达式之前执行一个简单的语句,这与C++、Java不同。这个语句中的短变量的作用域也只存在于if-else的作用域中。

if v:=math.Pow(x,n);v<lim {//小括号没有,大括号必须存在
return v
}
else{
  return v-1 //在if中声明的短变量仍然可以使用
}

也可以类似C++的if形式:

if x<0{//小括号没有,大括号必须存在
  fmt.Println(math.Sqrt(x))
}

分支:switch语句

go中的switch语句由上到下对case中的数值进行匹配,默认情况下只运行选中的case,如果在case语句最后添加fallthrough语句,则顺序执行直到一个没有出现fallthrough语句的case
正如在C++的switch语句中,只有遇到break才会停止执行一样,在go中,只有遇到fallthrough才会向下继续执行。
例如:

switch i:=0;i{
		case 0:
		fmt.Println("0")
		fallthrough
		case 1:
		fmt.Println("1")
		fallthrough
		default:
		fmt.Println("default")
	}

执行结果:

0
1
default

而:

switch i:=0;i{
		case 0:
		fmt.Println("0")
		fallthrough
		case 1:
		fmt.Println("1")
		
		default:
		fmt.Println("default")
	}

执行结果为:

0
1

go的switchif一样,可以在之前执行一个简单的语句;同for一样,可以不加任何表达式,这时等同于switch true,会在case中匹配第一个为true的分支。这样写,可以把if-then-else语句写得更加清晰。
例如:

t := time.Now()
	switch {//自动匹配第一个成立的case中的条件。
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}

不同于C++,go的switch语句不必要求case语句中一定是常量,也不必要求一定是整数。

defer

defer语句会将某个函数(语句)推迟到外层函数返回之后再执行。当遇到被defer标识的函数时,函数中的参数数值将会立即被确定,但是直到外层函数返回之前,都不会被执行。
例如:

func main() {
	i,j:=1,2
	defer fmt.Println("world",i,j)
	i+=1
	j+=2
	fmt.Println("hello")
}

执行结果为:

hello
world 1 2

所有被defer的函数,会按照遇到的先后顺序进入一个中,在外层函数返回时,被推迟的函数将会按照后进先出的顺序被调用。

更多关于defer的信息点击此处

更多类型

指针

类型*T是指向类型T的指针,零值是nil
指针的声明语句为var p *int,只是将普通变量的声明语句中的类型改变。
操作符&为取地址操作符:p=&i
操作符*为取内容操作符:m:=*p

与C++不同的是,go不提供指针运算,不存在*(p+1)

结构体

定义:
结构体就是一组字段,一个结构体的定义如下:

type Vertex struct{//一个名为Vertex的结构体
  X int
  Y int
}

初始化

v:=Vertex{1,2}//使用常量、大括号初始化
v1:=Vertex{X:1}//只赋值X,Y:0被隐式赋予
v2:=Vertex{}//零值

字段访问
使用点号.来访问

v.X=1

如果是指针,也可以使用隐式间接引用

p:=&Vertex{1,2}//创建一个*Vertex类型的指针
p.X=7//而不需要使用(*p).X

数组与切片

一个数组的类型由元素类型以及数组长度决定:

var a [10]int//声明一个包含10个整数的数组
a[0]=1
a[1]=2
b:=[3]int{1,2,3}//数组初始化

切片为数组元素提供了动态大小的、灵活的视角,切片的类型是[]T
切片使用两个下标来定界:a[low:high],他会选择一个区间,一共high-low个元素,包括下标为low的元素,但不包括下标为high的元素。一个数组可以表示为a[0:n]是一个长度为n的数组。
定义一个切片:

var s[]int = a[1:6]//表示a数组中从a[1]到a[5]的5个元素。

切片就像是数组的引用,其中不会存储任何数据,只是描述了一段底层数据。更改切片也会修改它引用的数组中的值,与它共享底层数据的切片都会观察到这些更改。切片的零值是nilnil切片的长度和容量都是0,而且没有底层数组。

切片文法类似于没有长度的数组文法:

s:=[]bool{true,false,true,true}//会创建一个长度为4的数组,然后创建引用它的切片s

切片允许不指定上界或下界,存在默认上界或下界,以下切片是等价的:

a[0:10]
a[:10]
a[0:]
a[:]

切片有长度容量,长度就是它所包含的元素个数,容量就是从它的第一个元素开始,到其底层元素末尾的个数。分别使用len(s)cap(s)来获取。
只要有足够的容量,就可以通过重新切片来扩展一个切片。如:

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)

有点类似于python中的切片。

创建切片需要使用make内建函数,这也是创建动态数组的方式。make将会分配一个元素是零值的数组并返回一个引用了它的切片:

a:=make([]int,5)//创建长度为5的int切片
b:=make([]int,0,5)//创建长度为0,容量为5的int切片
b=b[:cap(b)]//重新切片,长度为5,容量为5
b=b[1:]//重新切片,长度为4,容量是4

切片的切片:切片可以包括切片:

board := [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}

向切片追加元素:使用append函数。点击此处获取介绍append的详细文档。
append函数原型为:func append(s []T,vs...T)[]T,其中,第一个参数是被添加到的切片,其余的切片会被加到该切片s的末尾,如果s的底层数组较小,容量不足以添加所有新的追加数据,则会自动分配一个更大的数组,返回的切片会指向这个新分配的数组。

点击此处获取更多关于切片的内容。

Range

使用for循环的range形式来遍历切片或者映射,在for中使用range的时候,会返回两个值,第一个值为当前元素对应的下标,第二个值为该下表对应元素的一个副本。例如:

var powvalue=[]int{1,4,9,16,25}
for index,value := range powvalue{
  fmt.Printf("index and value is",index,value)
}

如果在使用中不需要index或者value,可以使用_来省略这个值:

for _,value:=range powvalue{
  fmt.Printf("value is",value))
}
for index,_:=range powvalue{

}

不过,如果只需要索引值(像切片范围),可以直接忽略第二个变量value而不需要使用_来替代。如:

for index:=range powvalue{

}

映射:map

map的创建与初始化

映射的类型为map[T1]T2,映射的零值为nil,零值映射nil既没有键,也不能添加键。
类似于切片,映射也使用make函数初始化映射,例如:

var m map[string]Vertex=make(map[string]Vertex)

映射的文法于结构体相似,但是需要指定键名:

var m=map[string]Vertex{
  "Key1": Vertex{1,2},
  "Key2":Vertex{3,4},//注意最后一个也需要有逗号喔
}

如果顶级类型只是一个类型名,可以在文法的元素中省略:

var m=map[string]Vertex{
  "Key1":{1,2},
  "Key2":{3,4},
}

map的修改

//对某个键插入或者修改值:
m[key]=elem
//获取元素
//如果该元素不存在,elem为类型对应的零值
elem=m[key]
//删除元素
delete(m,key)
//通过双赋值检测某个键是否存在
//如果key在m中,elem,ok为elem,true,否则为零值,false
//
elem,ok=m[key]//elem,ok已声明
elem,ok:=m[key]//elem,ok未声明

函数值

函数也是值,也可以像其他值一样传递,比如用作函数的参数或者返回值。
如下:

//compute函数接收一个函数fn作为参数
func compute(fn func(float64,float64)float64)float64{
	return fn(3,4)
}
func main(){
	hypot:=func(x,y float64)float64{
		return math.Sqrt(x*x+y*y)
	}
	value:=compute(hypot)
	valuep:=compute(math.Pow)
	valueh:=hypot(5,12)
	//
}

函数的闭包

go函数可以是一个闭包,闭包是一个函数值,它引用了函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,这些函数被这些变量“绑定”在一起。
例如:函数adder返回一个闭包,每个闭包都被绑定在其各自的sum变量之上。

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

方法与接口

方法

go没有类,不过允许为结构体定义方法,方法就是一类带有特殊的接收者参数的函数。例如:

//结构体
type Vertex struct{
	X,Y float64
}
//Vertex v作为接收者的方法
func (v Vertex) Abs()float{
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

除了结构体,还可以为非结构体类型定义方法:

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

方法被调用的时候,需要使用接收者类型进行调用。
接收者可以是普通类型,也可以是指针类型,不过,如果使用指针类型作为接收者,在函数中对接收者的操作将会反映到接收者本身,就类似于指针作为函数参数一样。比如:

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}
//可以直接使用值类型调用Scale,不过,其调用行为等同于(&v).Scale(2)

无论方法的接收者是指针类型还是值类型,都可以使用指针类型或者值类型的该类型变量去调用,程序将会自动解释,以方法声明为准。
比如:一个以指针类型为接收者的函数使用值类型调用,被自动解释为(&v).Scale(1),而如果是一个以值类型为接收者的函数使用指针类型调用,被自动解释为(*v).Scale(1)
方法的接收者类型定义必须与方法在同一个包内,接收者不能是int等内建类型

使用指针作为接收者,可以避免在每一次调用方法的时候都复制这个值。如果接收者是大型结构体,这样做会更加高效。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。

接口

接口类型是由一组方法签名定义的集合,接口类型的变量可以保存任何实现了这些方法的值。
接口的定义:

type Abser interface{
	Abs() float64
}

如果为某接受类型定义了方法Abs,则可以通过Abser类型来调用Abs的方法,根据Abser的具体类型,将会调用不同的Abs方法。
比如:

type MyFloat float64
type Vertex struct{
	X,Y float64
}
func (f MyFloat) Abs()float64{
	if f<0{
		return float64(-f)
	}
	return float64(f)
}
func (v*Vertex)Abs()float64{
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main(){
	var a Abser//一个接口a
	f:=MyFloat(2)
	v:=Vertex{3,4}
	a=f
	a.Abs()//f.Abs()
	a=&v
	a.Abs()//v.Abs()
}

类型通过实现一个接口的所有方法来实现该接口。没有专门的显式声明,也没有implements关键字。隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。

接口也是一个值,可以像其他值一样传递,可以作为函数的参数或者返回值。

接口可以看作是一个包含了值和具体类型的元组:(value,type),其中保存有一个具体底层类型的具体值。调用方法时执行底层类型作为接收者的同名方法。

如果接口内的具体值是nil,方法会被nil接收者调用,尽管在一些语言中这回出发空指针异常,但是go通常会写一些方法来优雅地处理这一情况,比如:

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

保存了nil值的接口本身并不是空值nil。空接口值nil既不保存值也不保存具体类型,如果试图使用nil接口调用方法,会出错,因为不知道究竟应该是什么具体的类型。

没有定义方法的接口叫做空接口,例如:

var i interface{}

空接口可以保存任意类型的值,因为任意一种类型都实现了该接口中的方法(没有方法),它被用来处理未知类型的值,例如,使用fmt.Pirnt来打印接口中的变量的信息。

有时候,需要得知某一接口中保存的具体类型,可以使用类型断言t:=i.(T)或者t,ok:=i.(T)。其中i是一个接口变量,T是一种类型名。
如果确定接口i中保存的就是T类型的变量,那么使用t:=i.(T),可以将对应类型的数据放在变量t中。但如果T不是实际存在的类型,上述语句就会引发一个panic
使用t,ok:=i.(T)来确定i中保存的变量类型是不是T,如果不是,ok将会为falsetT类型的零值,如果是ok将会为truet就是对应的T类型数据。
类型选择简化了判断过程,类型选择使用关键字type代替具体类型,使用switch语句来判断,例如:

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

fmt.Stringer接口

Stringer接口是fmt包中的一个接口:

type Stringer interface{
	String() string
}

很多类型都会实现该接口,使用接口来打印类型中的信息,比如:

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

fmt.error接口

Go使用error来表示错误状态,他也是一个内建接口:

type error interface{
	Error()string
}

通常函数会返回一个error值,调用它的代码应该判断error==nil判断执行是不是出错。也可以为某一错误类型实现Error方法来实现error接口,直接使用error来直接打印不同的错误信息。

io.Reader接口

接口表示从数据流的末尾进行读取。Go标准库包含该接口的许多实现,包括文件、网络连接、压缩、加密等等。
io.Rader接口中有一个Read方法,它会使用数据填充给定的字节切片和错误值,遇到数据流的末尾会返回一个io.EOF错误。它的原型是:

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

图像接口

image包定义了Image接口:

package image
type Image interface{
	ColorModel() color.Model
	Bounds()Reacangle
	At(x,y int) color.Color
}

阅读文档了解更多信息。

并发

Go程

Go程是Go运行时(runtime)管理的轻量级线程。
代码go func(x,y)会启动一个新的Go程并在该Go程中执行函数func(x,y)。Go程在相同的地址空间之中运行,因此在访问共享内存的时候必须进行同步。sync包提供了这种能力。不过,Go中还有其他的方法,这就是信道。

信道

信道是一种带有类型的管道,可以使用信道操作符来向其中发送数据,或接受其中的数据。

ch<-v//将数据v发送给信道ch
v:=<-ch//从ch接受一个值,赋给v

信道在使用之前应该创建:ch:=make(chan int)创建了一个可以放int型数据的信道。
默认情况下,发送和接受操作在另一端准备好之前都会阻塞,这使得Go程在没有显式锁或竟态变量的情况下也能同步。

信道可以有一个缓冲区,在创建信道的时候声明缓冲区大小:ch:=make(ch int,100),只有当信道的缓冲区填满的时候,向其中发送数据才会阻塞,缓冲区是空的时候,接收方会阻塞。

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

v, ok := <-ch

此时 ok 会被设置为 false。

循环 for i := range c 会不断从信道接收值,直到它被关闭。

注意: 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

注意: 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

select 语句使一个 Go 程可以等待多个通信操作。它会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。如果没有准备好的分支,default分支就会被执行,为了在尝试发送或者接受的时候不发生阻塞,可以使用default分支。比如:

x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		default:
			fmt.Println("    .")
			time.Sleep(50 * time.Millisecond)
		}
	}

sync.Mutex允许锁定一个变量,保证每次只有一个Go程可以访问一个共享变量,从而避免冲突。
这个概念叫做互斥,Mutex叫做互斥锁。
Go中提供了互斥锁类型sync.Mutex以及两个方法:LockUnlock
在代码调用之前,调用Lock方法,在调用之后调用Unlock方法来保证一段代码的互斥执行。参见Inc方法。也可以使用defer语句来保证互斥锁一定会被解锁,参见Value方法。

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
	v   map[string]int
	mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	c.v[key]++
	c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	defer c.mux.Unlock()
	return c.v[key]
}
posted @ 2019-10-16 12:26  梨可707  阅读(306)  评论(0编辑  收藏  举报