Chris's Blog

Keep Walking......

Go语言基础

Go语言常被拿来和C与Python进行比较,Go语言在语法上确实和C与Python有很多的相似性,不过这两种语言我都不太擅长,C语言仅局限在大学的课本上,Python倒是之前用来写过一些安装脚本。总之,我是不够资格去评论Go与这两种语言的差别了,在学习的过程中,倒是会与Java做些比较,其实多少也有些相似性。

package

package <pkgName> 定义当前文件属于哪个包。

包名如果是main,则表明它是一个可独立运行的包,它在编译后会产生可执行文件。除了main包之外,其它的包最后都会生成*.a文件(也就是包文件),并放置在$GOPATH/pkg/$GOOS_$GOARCH中(以Mac为例就是$GOPATH/pkg/darwin_amd64)。

每一个可独立运行的Go程序,必定包含一个package main,在这个main包中必定包含一个入口函数main,main函数既没有参数,也没有返回值。

包名和包所在的文件夹名可以是不同的,<pkgName> 即为通过 package <pkgName> 声明的包名,而非文件夹名。

import

import用来导入包文件。除了标准库之外,import还支持如下两种方式来加载自己写的模块:

1、相对路径

  import “./model”  //与当前文件同一目录的model目录,但是不建议使用 

2、绝对路径

  import “shorturl/model”  //加载gopath/src/shorturl/model模块 

import的几种特殊使用方式:

1、点操作

  import(
     . "fmt"
  )

点操作的含义就是这个包导入之后,在调用这个包的函数时,可以省略前缀的包名,也就是将 fmt.Println(“hello world”) 简写成 Println(“hello world”)

2、别名操作

  import(
      f "fmt"
  )

调用包函数时前缀变成了别名,即 f.Println(“hello world”)

3、_操作

 import (
     "database/sql"
     _ "github.com/ziutek/mymysql/godrv"
 )

_操作就是引入该包,而不直接使用包里面的函数,只是调用了该包里面的init函数。

变量

定义变量的标准方式:

 var varName type

定义多个变量:

 var vname1, vname2, vname3 type

定义变量并初始化值:

 var varName type = value

定义并初始化多个变量:

 var vname1, vname2, vname3 type= v1, v2, v3

可以忽略类型声明,Go会根据其值的类型来初始化。

简短声明:

 vname1, vname2, vname3 := v1, v2, v3

:= 取代了var和type,但只能用在函数内部,在函数外部使用会无法编译通过,所以一般用var方式来定义全局变量。

_(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。

另外需要注意的是,Go对于已声明但未使用的变量会在编译阶段报错

常量

定义常量的标准方式:

 const constantName type = value

类型声明可以忽略。

定义一组常量就可以作为enum:

 const(
    x = iota // x == 0
    y = iota // y == 1
    z = iota // z == 2
 )

这里的iota,在声明enum的时候采用,它默认开始值是0,每调用一次加1,每遇到一个const关键字,iota就会重置。

若一行定义多个常量的值为iota,则这些常量的值相同。

 const(
    x, y, z = iota, iota, iota  // x=0, y=0, z=0
 )

数据类型

Go语言中约定变量名或函数名的定义方式决定可访问性:

大写字母开头的变量是公用变量,其它包可以读取;小写字母开头是私有变量。

大写字母开头的函数也是一样,相当于Java中的public方法;小写字母开头的就是private方法。

Boolean

布尔值的类型为bool,值是true或false,默认为false。

数值类型

整数类型有无符号int和带符号uint两种。这两种类型的长度相同,但具体长度取决于不同编译器的实现,通常为32位。

Go里面也有直接定义好位数的类型:rune, int8, int16, int32, int64和byte, uint8, uint16, uint32, uint64。其中rune是int32的别名,byte是uint8的别名。这些类型的变量之间不允许互相赋值或操作,否则会在编译时报错。

浮点数类型有float32和float64两种(没有float类型),默认是float64。

复数类型有complex128(64位实数+64位虚数)和complex64(32位实数+32位虚数)两种。复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位。

字符串

Go中的字符串都是采用UTF-8字符集编码。字符串是用一对双引号 ” ” 或反引号 ` ` 括起来定义,它的类型是string。

Go的字符串是不可变的,可以使用 + 操作符来连接两个字符串。

多行的字符串可以通过 ` 来声明,` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,没有字符转义。

array

array数组的定义方式:

 var arr [n]type

n表示数组的长度,type表示存储元素的类型。对数组的操作都是通过 [] 来进行读取或赋值。

由于长度也是数组类型的一部分,因此[3]int与[4]int是不同的类型,数组不能改变其长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。

数组可以使用 := 来声明:

 a := [3]int{1, 2, 3}  // 声明了一个长度为3的int数组

 b := [10]int{1, 2, 3}  // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0

 c := [...]int{4, 5, 6}  // 可以省略长度而采用 "..." 的方式,Go会自动根据元素个数来计算长度 

Go支持嵌套数组,即多维数组。

slice

slice动态数组并不是真正意义上的动态数组,而是一个引用类型。slice总是指向一个底层的array,slice的声明也可以像array一样,只是不需要指定长度。

slice和array在声明时的区别:声明数组时,方括号内写明了数组的长度或使用…自动计算长度,而声明slice时,方括号内没有任何字符。

slice可以通过 array[i:j] 来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i。

slice的默认开始位置是0,ar[:n] 等价于 ar[0:n]

slice的第二个序列默认是数组的长度,ar[n:] 等价于 ar[n:len(ar)]

如果从一个数组里面直接获取slice,可以这样ar[:],因为默认第一个序列是0,第二个是数组的长度,即等价于 ar[0:len(ar)]

slice是引用类型,当引用改变其中元素的值时,其它的所有引用都会改变该值。

slice像一个结构体(struct),这个结构体包含了三个元素:

指针 - 指向数组中slice指定的开始位置。
长度 - slice的长度。
最大长度 - slice开始位置到数组的最后位置的长度。

slice的常用内置函数:

len - 获取slice的长度。
cap - 获取slice的最大容量。
append - 向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice。
copy - 从源slice中复制元素到目标dest,并且返回复制的元素的个数。

map

map也就是Python中的字典,它的格式为:

 map[keyType]valueType

map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取。

map的长度是不固定的,和slice一样,也是一种引用类型。

内置的len函数同样适用于map,返回map拥有的key的数量。

map的值可以很方便的修改,通过 numbers[“one”]=11 可以很容易的把key为one的字典值改为11。

map和其他基本类型不同,它不是thread safe的,在多个goroutine存取时,必须使用mutex lock进行加锁。

map可以通过key:val的方式初始化值,同时map内置有判断是否存在key的方式。可以通过delete删除map的元素。

流程控制

if

if条件判断语句中没有圆括号,但需要有花括号。

if语句里面允许声明变量,这个变量的作用域是该条件逻辑块内。

goto

用goto跳转到在当前函数内定义的标签。标签名是大小写敏感的。

for

for条件语句不需要括号。

若需要进行多个赋值操作,由于Go里面没有,操作符,则需同时赋值多个变量,如:*i, j = i+1, j-1

若条件语句中只存在一个条件,则 ; 也可以省略,相当于实现了while的功能,如:

 sum := 1

 for sum < 1000 {
    sum += sum
 }

break用于跳出当前循环,continue用于跳过本次循环。

break和continue还可以跟着标号,用来跳到多重循环中的外层循环。

for配合range可以用于读取slice和map的数据,如:

 for k,v:=range map {
    fmt.Println("map's key:",k)
    fmt.Println("map's val:",v)
 }   

由于Go支持 “多值返回”, 而对于 声明而未被调用 的变量, 编译器会报错, 在这种情况下, 可以使用 _ 来丢弃不需要的返回值,如:

 for _, v := range map{
    fmt.Println("map's val:", v)
 } 

switch

switch表达式没有类型要求。case语句中可使用多个值。

默认每个case执行完成都会break,匹配成功后不会自动向下执行其他case,但是可以使用fallthrough强制执行后面的case代码。

default用于无匹配条件的case。

函数

函数通过关键字func来声明,格式如下:

 func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
      // statement
      return value1, value2
  }

关键字func用来声明一个函数funcName;函数可以有一个或者多个参数,每个参数后面带有类型,通过,分隔;函数可以返回多个值。

返回值可以不声明变量只定义类型;如果只有一个返回值且不声明返回值变量,那么可以省略包括返回值的括号;如果没有返回值,那么就直接省略最后的返回信息;如果有返回值,那么必须在函数的外层添加return语句;如果定义了返回值变量,那么return语句可以不用带上变量名。

在Go中函数也是一种变量,可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型:

 type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...]) 

变参

Go函数支持变参,在函数体中,变参是一个slice。如:

 func myfunc(arg ...int) {} 

传值与传指针

当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。

传递引用是通过传指针来实现的。传指针使得多个函数能操作同一个对象。

传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。

Go语言中string,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。若函数需改变slice的长度,则仍需要取地址传递指针。

结构体struct

struct的声明方式:

 type structName struct {}

struct的使用方式:

1、按照顺序提供初始化值。例如:

 P := person{"Tom", 25}

2、通过field:value的方式初始化,这样可以任意顺序。例如:

 P := person{age:24, name:"Tom"}

3、通过new函数分配一个指针。例如此处P的类型为*person:

 P := new(person) 

定义struct的时候,若字段只提供类型,而不写字段名,它就是匿名字段,也称为嵌入字段。

当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。

包含匿名字段的struct,若定义了与匿名字段类型中同名的字段,则可实现覆盖。

method

定义method的格式:

 func (r ReceiverType) funcName(parameters) (results)

receiver可以为任何类型。若receiver为指针类型,Go会自动将传入的实例变量转为指针,反之亦然。

若匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method,通过这种方式实现method继承。

包含匿名字段的struct,若定义与匿名字段同名的method,则可实现method重写。

interface

interface是一组method的组合。

interface的声明方式:

 type interfaceName interface { }

空interface(interface{})不包含任何的method,可以用于存储任意类型的数值。一个函数把interface{}作为参数,那么他可以接受任意类型的值作为参数,如果一个函数返回interface{},那么也就可以返回任意类型的值。

在interface中定义interface类型的字段,即可继承该interface中的所有method。

类型判断

comma-ok判断变量的类型:

 value, ok = element.(T)

value是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

switch测试变量的类型:

 switch value := element.(type) {
        case int:
            fmt.Printf("element is an int and its value is %d\n", value)
        case string:
            fmt.Printf(“element is a string and its value is %s\n", value)
        default:
            fmt.Println("element is of a different type", index)
  }

element.(type)语法不能在switch外的任何逻辑里面使用,如果要在switch外面判断一个类型就要使用comma-ok。

内存分配

make用于内建类型(map、slice 和channel)的内存分配。

new用于各种类型的内存分配。new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。

make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。

错误处理

error

Go内置有一个error类型,专门用来处理错误信息。

自定义error类型可以通过实现error接口的Error方法。

defer

defer延迟语句,当函数执行到最后时,defer语句会按照逆序执行,最后该函数返回。

多个defer是采用后进先出模式。

Panic

panic 是一个内建函数,类似于异常处理,可以中断原有的控制流程。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。panic可以由直接调用panic产生,也可以由运行时错误产生。

Recover

recover 是一个内建的函数,可以让出现panic的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine出现panic,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

panic类似java中的throw exception,recover类似catch exception。

main() && init()

init函数能够应用于所有的package,main函数只能应用于package main。这两个函数在定义时不能有任何的参数和返回值。Go程序会自动调用init()和main(),所以你不需要主动调用这两个函数。每个package中的init函数是可选的,但package main就必须包含一个main函数。

虽然一个package里面可以写任意多个init函数,但从可读性和可维护性来说,建议在一个package中只有一个init函数。

程序的初始化和执行都起始于main包。如果main包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包中的包级常量和变量进行初始化,接着执行init函数(如果有的话)。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。

反射

反射需要先将值通过reflect.TypeOf或者reflect.ValueOf转化成reflect对象。

 t := reflect.TypeOf(i)    //得到类型的元数据
 v := reflect.ValueOf(i)   //得到实际的值

如果需要修改反射的字段,转换成reflect对象的时候需要传入指针。

并发处理

goroutine

goroutine本质就是线程,是通过Go的runtime管理的一个线程管理器。通过关键字go就可以启动一个goroutine。

默认情况下,调度器仅使用单线程,如果一个goroutine没有被阻塞,那么其它的goroutine就不会被执行。想要发挥多核处理器的并行,需要在我们的程序中显式调用 runtime.GOMAXPROCS(n) 来告诉调度器同时使用多个线程。

lock

对于多个线程的并发处理,需要加锁来保证操作的准确性。在包sync中提供了Mutex用于对代码块进行加锁和解锁。

atomic

为保证变量的原子操作,在包sync/atomic中提供了很多原子操作的方法用于确保线程安全。

channels

goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。goroutine之间使用channel进行数据的通信,可以通过它发送或者接收值。这些值只能是channel类型。定义一个channel时,也需要定义发送到channel的值的类型。

必须使用make 创建channel:

 channelName := make(chan type) 

channel通过操作符 <- 来接收和发送数据,例如:

 ch <- v    // 发送v到channel ch.
 v := <-ch  // 从ch中接收数据,并赋值给v 

默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得goroutine同步变的更加的简单,而不需要显式的lock。所谓阻塞,也就是如果读取(value := <-ch)将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。

Buffered Channels

Go允许指定channel的缓冲大小,就是channel可以存储多少元素。

例如:ch:= make(chan bool, 4)

创建了可以存储4个元素的bool型channel。在这个channel中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel中读取一些元素,腾出空间。

Buffered Channels的定义方式:

 channelName := make(chan type, value)

Range && Close

可以通过range,像操作slice一样操作缓存类型的channel。

for i := range c 能够不断的读取channel里面的数据,直到该channel被显式的关闭。生产者通过close函数关闭channel。关闭channel之后就无法再发送任何数据了,在消费方可以通过语法 v, ok := <-ch 测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。

一定要在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic。

channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的。

Select

如果存在多个channel的时候,通过select可以监听channel上的数据流动。

select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。

可以在select中设置超时,避免整个程序进入阻塞的情况。

例如:

 select {
     case v := <- c:
             println(v)
     case <- time.After(5 * time.Second):
             println("timeout")
             o <- true
             break
 }  

select的语法类似于switch,default就是当监听的channel都没有准备好的时候执行,这样就不会阻塞等待channel。

goroutine的相关函数

runtime包中有几个处理goroutine的常用函数:

Goexit

退出当前执行的goroutine,但是defer函数还会继续调用

Gosched

让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。

NumCPU

返回 CPU 核数量

NumGoroutine

返回正在执行和排队的任务总数

GOMAXPROCS

用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

Comments