Go语言学习笔记

Go语言总结



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等占位符和 、 、 转义也几乎完全一致。但Golang的Println不支持格式化,只有Printf支持,所以我们经常会在后面加入 换行。此外,Golang加入了%T打印值的类型,%v打印数组等集合的所有元素。

  4. Go语言基本类型

    bool
    string
    int int8 int16 int32 int64
    uint uint8 uint16 uint32 uint64 uintptr
    byte // uint8 的别名
    rune // int32 的别名
    // 表示一个 Unicode 码点
    float32 float64
    complex64 complex128

  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继承发扬的同时,也有自己的想法融入其中:
    5.1 if/switch/for 的条件部分都没有圆括号(可以写),但必须有花括号。
    5.2 switch的case中不需要break(默认break);
    5.3 如果case语句后,想继续下一个case语句执行,需加入fallthrogh
    5.4 没有条件的 switch 同 switch true 一样。
    5.5 switch的case条件可以是多个值。
    5.6 Golang中没有while。

  9. 分号和花括号()

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

  10. 函数()

    7.1 func关键字。
    7.2 最大的不同就是“倒序”的类型声明。
    7.3 不需要函数原型,引用的函数可以后定义。这一点很好,不类似C语言里要么将“最底层抽象”的函数放在最前面定义,要么写一堆函数原型声明在最前面。
    7.4 函数的定义:
    func 关键字 函数名(参数1..)(返回值1, 返回值2 ){
    函数体
    }
    如:
    func add(a int, b int)(ret int, err int){ return a+b, b }

    7.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) }

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

  11. 集合()基本数据结构 slice, struct

    Golang提供了数组和Map作为基本数据结构:

    • 8.1 数组中的元素会自动初始化,例如int数组元素初始化为0
    • 8.2 切片(借鉴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 ", sliceTmp) }
  • 8.3 切片就像数组的引用
    切片并不存储任何数据, 它只是描述了底层数组中的一段。
    更改切片的元素会修改其底层数组中对应的元素。
    与它共享底层数组的切片都会观测到这些修改。
    例如
    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]

  • 8.4 切片的初始化
    变量名 := []类型{...}
    例如
    a := []int{1, 2, 3 } s := []struct{ age int name string }{ {1, "xiaoming"}, {21. "xiaohua"}, {23, "nhao"}, ** 注意, 最后一个逗号不能省 ** }

  • 8.5 切片的长度与容量
    切片拥有 长度 和 容量 。
    切片的长度就是它所包含的元素个数
    切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
    切片 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 ", len(s), cap(s), s) } 输出: len = 0, cap = 6, value = [] len = 3, cap = 6, value = [12 5 3] len = 1, cap = 4, value = [3]

  • 8.6 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.7 用 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 ", 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]

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

  • 8.9 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 是该映射元素类型的零值。
    同样的,当从 映射 中读取某个不存在的键时,结果是 映射 的元素类型的零值。

  1. 指针和内存分配()

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

  2. 面向对象编程("不太懂")

    Golang的结构体跟C有几点不同:
     10.1 结构体可以有方法,其实也就相当于OOP中的类了。
      10.1.1 支持带名称的初始化。
      10.1.2 用指针访问结构中的属性也用”.”而不是”->”,指针就像Java中的引用一样。
      10.1.3 没有public,protected,private等访问权限控制。C也没有protected,C中默认是public的,private需要加static关键字限定。Golang中方法名大写就是public的,小写就是private的。
     10.2 同时,Golang支持接口和多态,而且接口有别于Java中继承和实现的方式,而是采取了类似Ruby中更为新潮的DuckType。只要struct与interface有相同的方法,就认为struct实现了这个接口。就好比只要能像鸭子那样叫,我们就认为它是一只鸭子一样。
     10.3 Go 没有类。然而,可以在结构体类型上定义方法。
     10.4 可以对包中的任意类型(以type定义的类)定义任意方法,而不仅仅是针对结构体。但是,不能对来自其他包的类型或基础类型定义方法。
     10.5 有时候我们需要将接受方法的对象定义为指针,这样可以有两个效果:

  1. 可以提高参数传递的效率,不用拷贝。
  2. 修改接收者指向的值。
  1. 异常处理 (不太懂)

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

  2. goroutine(协程)

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

  3. 原子操作()

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

  4. Channel管道()

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

  5. 缓冲流()

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


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,将取出的值,从地址中取出该值进行打印 ,因为主线程先运行完,等打印时所有的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


原文地址:https://www.cnblogs.com/yyx1-1/p/6548006.html