Go语言学习笔记

来源:互联网 发布:淘宝c店铺 编辑:程序博客网 时间:2024/05/16 19:55

Go 编程查看标准包函数方法: ctrl + . + h 或者: ctrl + . + g

基础知识

  1. 运行方式()
    Golang提供了go run“解释”执行和go build编译执行两种运行方式,所谓的“解释”执行其实也是编译出了可执行文件后才执行的。

  2. Package管理()
    Golang约定:我们可以用./或../相对路径来引自己的package;如果不是相对路径,那么go会去$GOPATH/src下查找。

  3. 格式化输出()
    类似C、Java等语言,Golang的fmt包提供了格式化输出功能,而且像%d、%s等占位符和\t、\r、\n转义也几乎完全一致。但Golang的Println不支持格式化,只有Printf支持,所以我们经常会在后面加入\n换行。此外,Golang加入了%T打印值的类型,%v打印数组等集合的所有元素。

  4. Go语言基本类型

    • 值类型(基础类型)
      • 0.rune // int32 的别名
        1.bool,一个字节,值是true或者false,不可以用0或者1表示(java中boolean占用4个字节,而boolean作为数组出现时,每个boolean占用1个字节)
        2.int/uint(带符号为与不带符号位的int类型):根据平台不同是32位或者64位
        3.intx/uintx:x代表任意位数,例如:int3,代表占3bit的int类型
        4.byte占用8位,一个字节,相当于uint8,不带符号位
        5.floatx:由于没有double类型,所以float64就是double。float32小数精确到7位,float64小数精确到15位。
        6.complex64/complex128:复数类型
        7.uintptr:保存指针用的类型,也是随着平台改变而改变,因为指针的长度就是随平台而变。
        8.其他类型值:array,struct,string
    • 引用类型
      • slice,map,chan
    • 接口类型
      • interface
    • 函数类型
      • func
  5. 变量和常量()
    1.虽然Golang是静态类型语言,却用类似JavaScript中的var关键字声明变量。而且像同样是静态语言的Scala一样,支持类型自动推断。有一点很重要的不同是:如果明确指明变量类型的话,类型要放在变量名后面。这有点别扭吧?!后面会看到函数的入参和返回值的类型也要这样声明。
    2.短变量声明
    在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。
    注意: 函数外的每个语句都必须以关键字开始( var 、 func 等等), 因此 := 结构不能在函数外使用。

  6. 类型转换
    表达式 T(v) 将值 v 转换为类型 T.
var i int = 42 var f float64 = float64(i)var u uint = uint(f) //或者这样写 i := 42 f := float64(i) u := uint(f)


7. 常量
常量的声明与变量类似,只不过是使用 const 关键字。
常量可以是字符、字符串、布尔值或数值。
常量不能用 := 语法声明。

const ( f = 12i ) func main(){ const ( a = 2 b = 3.12 c = true d = "sssss" )  fmt.Println(a, b, c, d, f) }


8. 控制语句:
作为最基本的语法要素,Golang的各种控制语句也是特点鲜明。在对C继承发扬的同时,也有自己的想法融入其中;
* if/switch/for 的条件部分都没有圆括号(可以写),但必须有花括号。
* switch的case中不需要break(默认break);
* 如果case语句后,想继续下一个case语句执行,需加入fallthrogh
* 没有条件的 switch 同 switch true 一样。
* switch的case条件可以是多个值。
* Golang中没有while。

9. 分号和花括号()
注意: “分号和花括号 “
分号由词法分析器在扫描源代码过程自动插入的,分析器使用简单的规则:如果在一个新行前方的最后一个标记是一个标识符(包括像int和float64这样的单词)、一个基本的如数值这样的文字、或break continue fallthrough return ++ – ) }中的一个时,它就会自动插入分号。 分号的自动插入规则产生了“蝴蝶效应”:所有控制结构的左花括号不都能放在下一行。因为按照上面的规则,这样做会导致分析器在左花括号的前方插入一个分号,从而引起难以预料的结果。所以Golang中是不能随便换行的。

10. 函数;
* 1. func关键字。
* 2. 最大的不同就是“倒序”的类型声明。
* 3. 不需要函数原型,引用的函数可以后定义。这一点很好,不类似C语言里要么将“最底层抽象”的函数放在最前面定义,要么写一堆函数原型声明在最前面。
* 4. 函数的定义:
func 关键字 函数名(参数1..)(返回值1, 返回值2 ){
函数体
}

如:
func add(a int, b int)(ret int, err int){ return a+b, b }
* 5. 函数也是值,它们可以像其它值一样传递。函数值可以用作函数的参数或返回值。

func main() {     toSqrt := func(x, y float64)float64 { return math.Sqrt(x*x + y*y) }      fmt.Println(toSqrt(12, 5))      fmt.Println(autoSqrt(toSqrt))      fmt.Println(autoSqrt(math.Pow))  }  func autoSqrt(fn func(x, y float64) float64) float64 {     return fn(4, 3) }


* 6. 函数的闭包
Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。 该函数可以访问并赋予其引用的变量的值,换句话说,该函数被“绑定”在了这些变量上。

11. 集合()基本数据结构 slice, struct
* 1. Golang提供了数组和Map作为基本数据结构:
* 2. 数组中的元素会自动初始化,例如int数组元素初始化为0
* 3. 切片(借鉴Python)的区间跟主流语言一样,都是 “左闭右开”, 用 range()遍历数组和Map
例如:

func test02() {     source := []int{1, 2, 3, 4, 5, 6}     var sliceTmp []int = source[2:5] //注意 [2:n] 它为左闭右开, 例子即使: 从 下标2 开始至 下标4      fmt.Printf("%v\n", sliceTmp)}


* 4. 切片就像数组的引用
切片并不存储任何数据, 它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
与它共享底层数组的切片都会观测到这些修改。
例如

func main() {     source := [6]int{1, 2, 3, 5, 4}     var s []int = source[2:6]     fmt.Println(s)     source[5] = 7     fmt.Println(s)     s[0] = 88 fmt.Println(source) } 输出: [3 5 4 0] [3 5 4 7] [1 2 88 5 4 7]


* 5. 切片的初始化
变量名 := []类型{…}
例如
a := []int{1, 2, 3 }
s := []struct{ age int name string }{ {1, "xiaoming"}, {21. "xiaohua"}, {23, "nhao"}, } //注意, 最后一个逗号不能省
* 6. 切片的长度与容量
切片拥有 长度 和 容量 。
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。

func main() {     // a 是切片     a := []int{12, 5, 3, 6, 8, 6}     // 让切片的长度为 0     a = a[:0]     printSlice(a)     // 扩充切片的长度     a = a[:3]     printSlice(a)     // 丢掉开始的两个元素     a = a[2:]     printSlice(a) } func printSlice(s []int) {     fmt.Printf("len = %d, cap = %d, value = %v\n", len(s), cap(s), s) } 输出: len = 0, cap = 6, value = []     len = 3, cap = 6, value = [12 5 3]     len = 1, cap = 4, value = [3]


* 7. nil 切片
切片的零值是 nil 。
nil 切片的长度和容量为 0 且没有底层数组。
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
输出: [] 0 0 nil!
* 8. 用 make 创建切片
切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。
make 函数会分配一个元素为零值的数组并返回一个引用了它的切片.

func main() {    a := make([]int, 6)    printSlice("a", a)    b := make([]int, 0, 5)    printSlice("b", b)    c := make([]int, 3, 5)    printSlice("c", c)    d := b[:2]    printSlice("d", d)    e := d[2:5]    printSlice("e", e)} func printSlice(flag string, s []int) {    fmt.Printf("%s, len = %d, cap = %d, value = %v\n", flag, len(s), cap(s), s)} 输出: a, len = 6, cap = 6, value = [0 0 0 0 0 0]     b, len = 0, cap = 5, value = []     c, len = 3, cap = 5, value = [0 0 0]    d, len = 2, cap = 5, value = [0 0]    e, len = 3, cap = 3, value = [0 0 0]


* 9. Go的数组 与 C语言数组的区别
Go的数组是值语义。一个数组变量表示整个数组,它不是指向第一个元素的指针(不像 C 语言的数组)。
当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。
(为了避免复制数组,你可以传递一个指向数组的指针,但是数组指针并不是数组。)
可以将数组看作一个特殊的struct,结构的字段名对应数组的索引,同时成员的数目固定.

12. map结构的使用
例如:

type People struct {     age int    name string} var m map[string]People func test04() {     m = make(map[string]People)     fmt.Println(m)     m["Afra55"] = People{ 22, "Victor", }     m["xiaohuo"] = People{ 24, "nihao", } //注意:这种写法,最后一个','一定不能少,否则为语法错误      fmt.Println(m)     fmt.Println(m["Afra55"])} 输出: map[]     map[Afra55:{22 Victor} xiaohuo:{24 nihao}]    {22 Victor}


* 修改 map 映射
在映射 m 中插入或修改元素: m[key] = elem
获取元素: elem = m[key]
删除元素: delete(m, key)
通过双赋值检测某个键是否存在:elem, ok = m[key]
若 key 在 m 中, ok 为 true ;否则, ok 为 false
若 key 不在映射中,那么 elem 是该映射元素类型的零值
同样的,当从 映射 中读取某个不存在的键时,结果是 映射 的元素类型的零值

13. 指针和内存分配()
Golang中可以使用指针, 并提供了两种内存分配机制:
* new:分配长度为0的空白内存,返回类型T*。new 返回的是一个函数指针。
* make:仅用于 切片、map、chan消息管道,返回类型T而不是指针.

14. 面向对象编程
Golang的结构体跟C有几点不同:
 * 1. 结构体可以有方法,其实也就相当于OOP中的类了。
 * 2. 支持带名称的初始化。
 * 3. 用指针访问结构中的属性也用”.”而不是”->”,指针就像Java中的引用一样。
* 4. 没有public,protected,private等访问权限控制。C也没有protected,C中默认是public的,private需要加static关键字限定。Golang中方法名大写就是public的,小写就是private的。
 * 5. 同时,Golang支持接口和多态,而且接口有别于Java中继承和实现的方式,而是采取了类似Ruby中更为新潮的DuckType。只要struct与interface有相同的方法,就认为struct实现了这个接口。就好比只要能像鸭子那样叫,我们就认为它是一只鸭子一样。
* 6. Go 没有类。然而,可以在结构体类型上定义方法。
* 7. 可以对包中的任意类型(以type定义的类)定义任意方法,而不仅仅是针对结构体。但是,不能对来自其他包的类型或基础类型定义方法。
* 8. 有时候我们需要将接受方法的对象定义为指针,这样可以有两个效果:
1) 可以提高参数传递的效率,不用拷贝。
2) 修改接收者指向的值。

15. 异常处理
* 1. Golang中异常的使用比较简单,可以用errors.New创建错误返回值,也可以实现Error接口的方法来自定义异常类型,同时利用函数的多返回值特性可以返回异常类。
* 2. 比较复杂的是defer和recover关键字的使用。Golang没有采取try-catch“包住”可能出错代码的这种方式,而是用 延迟处理的方式.
* 3. 用defer调用的函数会以后进先出(LIFO)的方式,在当前函数结束后依次顺行执行。defer的这一特点正好可以用来处理panic。当panic被调用时,它将立即停止当前函数的执行并开始逐级解开函数堆栈,同时运行所有被defer的函数。如果这种解开达到堆栈的顶端,程序就死亡了。
* 4. 但是,也可以使用内建的recover函数来重新获得Go程的控制权并恢复正常的执行。由于仅在解开期间运行的代码处在被defer的函数之内,recover仅在被延期的函数内部才是有用的。
* 5. defer语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

func main() {     defer sqrt(9)     fmt.Println("9 * 9: ")} func sqrt(x float64) {     fmt.Println(x * x)}  打印的值: 9 * 9: 81


16. goroutine(协程)
* 1. goroutine使用Go关键字来调用函数,也可以使用匿名函数。可以简单的把go关键字调用的函数想像成pthread_create。也就是说goroutine阻塞时,Golang会切换到其他goroutine执行,这是非常好的特性!Java对类似
goroutine这种的协程没有原生支持,像Akka最害怕的就是阻塞。
* 2. 因为协程不等同于线程,操作系统不会帮我们完成“现场”保存和恢复,所以要实现goroutine这种特性,就要模拟操作系统的行为,保存方法或函数在协程“上下文切换”时的Context,当阻塞结束时才能正确地切换回来。
* 3. 像Kilim等协程库利用字节码生成,能够胜任,而Akka完全是运行时的。
* 4. 注意:”如果你要真正的并发,需要调runtime.GOMAXPROCS(CPU_NUM)设置;
* 5. 自己的观察: Go程类似分割的线程,执行完后, 从自己的函数中就直接退出, 不会回到主进程空间,同时不需要回收资源”

17. 原子操作
像Java一样,Golang支持很多CAS操作。运行结果是unsaftCnt可能小于200,因为unsafeCnt++在机器指令层面上不是一条指令,而可能是从内存加载数据到寄存器、执行自增运算、保存寄存器中计算结果到内存这三部分,所以不进行保护的话有些更新是会丢失的。

18. Channel管道()
* 1. 通过前面可以看到,尽管goroutine很方便很高效,但如果滥用的话很可能会导致并发安全问题。而Channel就是用来解决这个问题的,它是goroutine之间通信的桥梁,类似Actor模型中每个Actor的mailbox。多个goroutine要修改一个状态时,可以将请求都发送到一个Channel里,然后由一个goroutine负责顺序地修改状态。
* 2. Channel默认是阻塞的,也就是说select时如果没有事件,那么当前goroutine会发生读阻塞。同理,Channel是有大小的,当Channel满了时,
发送方会发生写阻塞。Channel这种阻塞的特性加上goroutine可以很容易就能实现生产者-消费者模式。
* 3. 用case可以给Channel设置阻塞的超时时间,避免一直阻塞。而default则使select进入无阻塞模式。
* 5. 有缓存管道与无缓存管道的区别:
* 对于无缓冲的channel,放入操作和取出操作不能再同一个routine中,而且应该是先确保有某个routine对它执行取出操作,然后才能在另一个routine中执行放入操作
* 在使用带缓冲的channel时一定要注意放入与取出的速率问题
* 使用channel控制goroutine数量

19. 缓冲流()
* 1. Golang的bufio包提供了方便的缓冲流操作,通过strings或网络IO得到流后,用bufio.NewReader/Writer()包装:
* 2. 缓冲区:Peek()或Read时,数据会从底层进入到缓冲区。缓冲区默认大小为4096字节。
* 3. 切片和拷贝:Peek()和ReadSlice()得到的都是切片(缓冲区数据的引用)而不是拷贝,所以更加节约空间。但是当缓冲区数据变化时,切片也会随之变化。而ReadBytes/String()得到的都是数据的拷贝,可以放心使用。
* 4. Unicode支持:ReadRune()可以直接读取Unicode字符。有意思的是Golang中Unicode字符也要用单引号,这点与Java不同。
* 5. 分隔符:ReadSlice/Bytes/String()得到的包含分隔符,bufio不会自动去掉。
* 6. Writer:对应地,Writer提供了WriteBytes/String/Rune。
* 7. undo方法:可以将读出的字节再放回到缓冲区,就像什么都没发生一样。

20. 数则和切片的异同:
* 声明数组和切片时的区别:
* 声明数组时, 方括号内包含了数组的长度或者 以动态计算数组的长度; var Myarray = [3]int{1, 2, 3}
* 声明切片时, 方括号内没有任何字符; var Myslice []int
* 切片的信息:
* 切片的长度: 为 两个序列号的减值;
* 切片的容量: 如果该切片直接基于数组创建, 它的容量为 起始位置至数组最后一位的元素个数; 即相减
* 如果该切片基于切片创建, 它的容量为 起始位置至母切片的容量的元素个数; 即相减

func main(){    var Myarray = [10]int{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}    aslice := Myarray[2:7]    fmt.Println("aslice lenth : ", len(aslice), " cap : ", cap(aslice))   //lenth : 5, cap : 8    bslice := Myarray[2:4]    fmt.Println("bslice lenth : ", len(bslice), " cap : ", cap(bslice))   //lenth : 2, cap : 6}


* 基于数组创建的切片, 切片的修改在不变化本身容量的情况下会影响到底层数组; 如果本身容量产生变化, 语言底层将会另外分配一块内存去存储该切片, 此时就不会影响到该数组了;

func main(){    var Myarray = [10]int{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}    aslice := Myarray[2:7]    aslice[0] = 99   //切片的容量为变化    fmt.Println(aslice, Myarray )  //[99 4 5 6 7] [1 2 99 4 5 6 7 8 9 10]    aslice = append(aslice, 3, 3, 3, 3, 3, 3, 3, 3) //切片的容量增大, 底层重新分配了一块内存;    fmt.Println(aslice, Myarray ) //[3 4 5 6 7 3 3 3 3 3 3 3 3] [1 2 3 4 5 6 7 8 9 10]}


21. make 与 new 的区别:
* make用于内建类型(slice, map, channel)的内存分配.
* new用于各种类型的内存分配;
* new(T) 分配了零值填充的T 类型的内存空间, 并且返回其地址(指针), *T 类型的值; new返回的是指针
* make(T, args), make 只能创建slice, map, channel, 并且返回一个有初始值(Go语言中, 每种类型的初始值并不相同)的的T类型, 注意: 非*T 类型. 本质上讲: 导致这三个类型有所不同的原因是: 指向数据结构的引用在使用前必须被初始化.
*


Go 语言开发中的坑

  1. slice(切片) 的坑:
    因为:当原始切片的容量不够,增加后,新的切片指向了一个新的地址,开发者此时极容易使用 slice 特性,导致返回的结果不是所期望的。
 //1.错误写法 func test(s []int){         s.append(s, 3);      //此时slice将与传入的slice指向不同内存地址,所以想得到理想的结果,就需要将新的slice地址传出。}func main(){    s := make([]int, 0)     //创建一个容量为 0 的slice;    fmt.Println(s)    test(s)                 //对这个创建的slice进行扩容    fmt.Println(s)}打印结果为: [0] [0]//2.正确写法 func test(s []int) []int {     s.append(s, 3)     return s } func main(){      s := make([]int, 0)      fmt.Println(s)      s = test(s)      fmt.Println(s)  } 打印结果为: [0] [3]

所以如果在操作slice时, 可能会使容量增大, 此时就一定要把新的slice返回出来。

2. time时间的坑
* 因为:Go语言的设计时,提供了一组常量layout, 来格式化它的时间输出。但是,但是:要么使用layout中提供的常量,要么直接拷贝它的常量字符串,千万不要对它的字符串进行修改,否则或造成输出时间不正确。

3. for range 与闭包函数的坑

//1.错误写法func closures() {    s := make([]int, 3)    for i := 0; i < 3; i++ {        s[i] = i + 1    }    for _, v := range s {   //轮询切片s,将取出的值,从地址中取出该值进行打印 ,因为range主线程先运行完,等打印时所有的v都已变为最后一个元素的地址,所以打印全是 3        go func() {            fmt.Println(v)        }()    }}打印结果为: 3 3 3//2.正确的写法func closures() {    s := make([]int, 3)    for i := 0; i < 3; i++ {        s[i] = i + 1    }    for _, v := range s {   //轮询切片s,将取出的值以值传递的方式传入闭包函数中;        go func(v int) {            fmt.Println(v)        }(v)    }}打印结果为: 1 2 3

Go 博客

  1. golang编程经验总结 http://studygolang.com/articles/2331

Go书籍

  1. Go web编程;
  2. Go语言编程
原创粉丝点击