Go学习笔记

本文是关于Go语言的学习笔记,介绍了Go语言的关键特性和使用技巧。内容涵盖Go的语法特性,如没有隐式转换、没有继承和泛型,以及Go的官方资源。此外,还详细讲解了Go的命令行工具、变量声明、循环、初始化函数、包的访问规则、位操作、数组和切片、map的使用注意事项、函数和方法声明、错误处理、并发控制等核心概念。特别强调了Go中错误处理的常见策略和defer语句的用法,以及接口和空接口类型的概念。
摘要由CSDN通过智能技术生成

本博客只是记录我在学习go语言时的知识点,之前用的是C++

 

  1. 它没有隐式的数值转换,没有构造函数和析构函数,没有运算符重载,没有默认参数,也没有继承,没有泛型,没有异常,没有宏,没有函数修饰,更没有线程局部存储。
  2. 最佳的帮助信息来自Go语言的官方网站,https://golang.org ,它提供了完善的参考文档,包括编程语言规范和标准库等诸多权威的帮助信息。同时也包含了如何编写更地道的Go程序的基本教程,还有各种各样的在线文本资源和视频资源,它们是本书最有价值的补充。Go语言的官方博客 https://blog.golang.org 会不定期发布一些Go语言最好的实践文章,包括当前语言的发展状态、未来的计划、会议报告和Go语言相关的各种会议的主题等信息(译注:http://talks.golang.org/ 包含了官方收录的各种报告的讲稿) 。 基于 Playground 构建的 Go Tour,https://tour.golang.org ,是一个系列的Go语言入门教程,它包含了诸多基本概念和结构相关的并可在线运行的互动小程序。 
  3. run。这个命令编译一个或多个以.go结尾的源文件,链接库文件,并运行最终生成的可执行文件

  4. build 这个命令生成一个可执行的二进制文件,之后你可以随时运行它,不需任何处理。 

  5. import 声明必须跟在文件的 package 声明之后。 

  6. i++ 和i-- 是语句而不像C系的其它语言那样是表达式。所以 j = i++ 非法,而且++和--都只能放在变量名后面,因此 --i 也非法。 

  7. Go语言只有for循环这一种循环语句。 

  8. Go语言不允许使用无用的局部变量(local variables) ,因为这会导致编译错误 

  9. 空标识符 (blank identifier) ,即 _ (也就是下划线) 可用于任何语法需要变量名但程序逻辑不需要的时候, 例如, 在循环里,丢弃不需要的循环索引, 保留元素值。 

  10. switch不带操作对象时默认用true值代替,然后将每个case的表达式和true值进行比较 

  11. 数值类型变量对应的零值是0,布尔类型变量对应的零值是false,字符串类型对应的零值是空字符串,接口或引用类型(包括slice、map、chan和函数) 变量对应的零值是nil。数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。 bytes.Buffer类型,结构体初始值就是一个随时可用的空缓存 。sync.Mutex的零值也是有效的未锁定状态。 结构体没有任何成员的话就是空结构体,写作struct{}

  12. 简短变量声明语句中必须至少要声明一个新的变量,简短变量声明语句对已经声明过的变量就只有赋值行为。

  13. 在Go语言中,返回函数中局部变量的地址也是安全的。 

  14. 编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。 

  15. 元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。 如:
    x, y = y, x 

  16. 包的初始化首先是解决包级变量的依赖顺序,然后安照包级变量声明出现的顺序依次初始化。

  17. init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。 

  18. 任何在在函数外部(也就是包级语法域) 声明的名字可以在同一个包的任何源文件中访问的,当前包的其它源文件无法访问在当前源文件导入的包。 

  19. 位操作运算符 &^ 用于按位置零(ANDNOT) :表达式 z = x &^ y ,如果对应y中bit位为1的话,结果对应z的bit位为0,否则对应的bit位等于x相应的bit位的值。

  20. 数组传参与C++不同时值传递,其实在Go语言中,所有的函数参数都是值拷贝传入的,函数参数将不再是函数调用时的原始变量。

  21. 字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:
    s := "left foot"
    t := s
    s +=", right foot" 

  22. 数组和结构体都是有固定内存大小的数据结构。slice和map则是动态的数据结构,它们将根据需要动态增长。 

  23. 指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。

  24. 复制一个slice只是对底层的数组创建了一个新的slice别名 。

  25. 如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断。因为一个nil值的slice的长度和容量都是0,但是也有非nil值的slice的长度和容量也是0的,例如[]int{}或make([]int, 3)[3:]。

  26. 内置的copy函数可以方便地将一个slice复制另一个相同类型的slice。copy函数的第一个参数是要复制的目标slice,第二个参数是源slice,目标和源的位置顺序和 dst = src 赋值语句是一致的。两个slice可以共享同一个底层数组,甚至有重叠也没有问题。copy函数将返回成功复制的元素的个数(我们这里没有用到) ,等于两个slice中较小的长度,所以我们不用担心覆盖会超出目标slice的范围。 

  27. 通常我们并不知道append调用是否导致了内存的重新分配,因此我们也不能确认新的slice和原始的slice是否引
    用的是相同的底层数组空间。同样,我们不能确认在原先的slice上的操作是否会影响到新的slice。因此,通常是将append返回的结果直接赋值给输入的slice变量:
    runes = append(runes, r)
    更新slice变量不仅对调用append函数是必要的,实际上对应任何可能导致长度、容量或底层数组变化的操作都是必要的。要正确地使用slice,需要记住尽管底层数组的元素是间接访问的,但是slice对应结构体本身的指针、长度和容量部分是直接访问的。 

  28. 虽然浮点数类型也是支持相等运算符比较的,但是将浮点数用做key类型则是一个坏的想法,正如第三章提到的,最坏的情
    况是可能出现的NaN和任何浮点数都不相等(两个NaN是不相等的)。 

  29. map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:
    _ = &ages["bob"] // compile error: cannot take address of map element
    禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

  30. Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。 

  31. map上的大部分操作,包括查找、删除、len和range循环都可以安全工作在nil值的map上,它
    们的行为和一个空的map类似。但是向一个nil值的map存入元素将导致一个panic异常(即在向map存数据前必须先创建map) 

  32. 内置的make函数可以创建一个map:
    ages := make(map[string]int) // mapping from strings to ints
    我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:
    ages := map[string]int{
    "alice": 31,
    "charlie": 34,
    }
    这相当于
    ages := make(map[string]int)
    ages["alice"] = 31
    ages["charlie"] = 34
    因此,另一种创建空的map的表达式是 map[string]int{} 

  33. 通过key作为索引下标来访问map将产生一个value。如果key在map中是存在的,那么将得到
    与key对应的value;如果key不存在,那么将得到value对应类型的零值 。但是有时候可能需要知道对应的元素是否真的是在map之中。例如,如果元素类型是一个数字,你可以需要区分一个已经存在的0,和不存在而返回零值的0,可以像下面这样测试:
    age, ok := ages["bob"]
    if !ok { /* "bob" is not a key in this map; age == 0. */ } 或者
    if age, ok := ages["bob"]; !ok { /* ... */ } 

  34. 和slice一样,map也是索引(一个哈希表的索引),map之间也不能进行相等比较;唯一的例外是和nil进行比较。 

  35. Go语言中并没有提供一个set类型,但是map中的key也是不相同的,可以用map实现类似set的功能。

  36. 有时候我们需要一个map或set的key是slice类型,但是map的key必须是可比较的类型,但是slice并不满足这个条件。不过,我们可以通过两个步骤绕过这个限制。第一步,定义一个辅助函数k,将slice转为map对应的string类型的key,确保只有x和y相等时k(x) == k(y)才成立。然后创建一个key为string类型的map,在每次对map操作时先用k辅助函数将slice转化为string类型。 

  37. 结构体成员的输入顺序也有重要的意义。交换成员出现的先后顺序,就是定义了不同的结构体类型。

  38. 一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适应于数组。) 但是S类型的结构体可以包含 *S 指针类型的成员 (和C++中的类一样)

  39. 结构体初始化:

    type Point struct{ X, Y int }
    p := Point{
    1, 2} (因为限制条件多,用的少)或
    p := Point{X : 1, Y:2} (两者不能混用)
    也可以取地址:pp := &Point{1, 2} 等价于
    pp := new(Point)
    *pp = Point{12
  40. 一次函数调用返回错误时,常用的五种处理方式 :
    a)最常用的方式是传播错误。一般而言,被调函数f(x)会将调用信息和参数信息作为发生错误时的上下文放在错误信息中并返回给调用者,调用者需要添加一些错误信息中不包含的信息。
    b)重新尝试失败的操作 。如果错误的发生是偶然性的,或由不可预知的问题导致的。一个明智的选择是重新尝试失败的操作。在重试时,我们需要限制重试的时间间隔或重试的次数,防止无限制的重试。
    c)输出错误信息并结束程序。 这种策略只应在main中执行。对库函数而言,应仅向上传播错误,除非该错误意味着程序内部包含不一致性,即遇到了bug,才能在库函数中结束程序。
    d)只需要输出错误信息
    e)直接忽略掉错误 

  41. 在Go中,函数被看作第一类值(first-class values) :函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。函数类型的零值是nil。调用值为nil的函数值会引起panic错误。函数值可以与nil比较 ,但是函数值之间是不可比较的 

  42. 拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal) ,我们可绕过这一限制,在任何表达式中表示一个函数值。函数值字面量是一种表达式,它的值被称为匿名函数(anonymous function) ,且在函数中定义的内部函数可以引用该函数的变量 。

  43. 当匿名函数需要被递归调用时,我们必须首先声明一个变量,再将匿名函数赋值给这个变量。 

  44. 在声明可变参数函数时,需要在参数列表的最后一个参数类型之前加上省略符号“...”,这表示该函数会接收任意数量的该类型参数(只能出现在最后一个参数)。 如:func errorf(format string, num ...int ){。。。} 。调用者隐式的创建一个数组,并将原始参数复制到数组中,再把数组的一个slice作为参数传给被调函数 。如果实参本来就是个slice类型,则需在最后一个参数后加上省略符。 如:
    values := []int{1, 2, 3, 4}
    errorf("sss", values ...)

  45. 普通函数或方法前加上关键字defer,就完成了defer所需要的语法。当defer语句被执行时,跟在defer后面的函数会被延迟执行。直到包含该defer语句的函数执行完毕时,defer后的函数才会被执行,不论包含defer语句的函数是通过return正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。 

  46. 1)defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。 通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的defer应该直接跟在请求资源的语句后。 2)调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。 例子:
    func bigSlowOperation() {
        defer trace("bigSlowOperation")() // don't forget the extra parentheses
        // ...lots of work…
        time.Sleep(10 * time.Second) // simulate slow
        operation by sleeping
    }

    functrace(msg string) func() {
        start := time.Now()
        log.Printf(
    "enter %s", msg)
        return func() {
        log.Printf(
    "exit %s (%s)", msg,time.Since(start))
        }
    }

    需要注意一点:不要忘记defer语句后的圆括号,否则本该在进入时执行的操作会在退出时执行,而本该在退出时执行的,永远不会被执行。
    每一次bigSlowOperation被调用,程序都会记录函数的进入,退出,持续时间。 

  47. 在循环体中的defer语句需要特别注意,因为只有在函数执行完毕后,这些被延迟的函数才会执行。 这就要注意内存消耗了。如
    for _, filename := range filenames {
    f, err := os.Open(filename)

    if err != nil {
    return err
    }
    d
    eferf.Close() // NOTE: risky; could run out of file
    descriptors
    // ...process f…
    }
    这就会导致系统的文件描述符耗尽 ,可以用以下方法解决:将循环体中的defer语句移至另外一个函数。在每次循环时,调用这个函数。
    for _, filename := range filenames {
    if err := doFile(filename); err != nil {
    return err
    }
    }
    f
    uncdoFile(filename string) error {
    f, err := os.Open(filename)

    if err != nil {
    return err
    }
    d
    eferf.Close()
    // ...process f…
    } 

  48. 在Go的panic机制中,延迟函数的调用在释放堆栈信息之前。 

  49. 如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。 

  50. 在函数声明时,在其名字之前放上一个变量,即是一个方法。这个附加的参数会将该函数附加到这种类型上,即相当于为这种类型定义了一个独占的方法。 附加的参数,叫做方法的接收器(receiver) 。接收器.函数名叫做选择器(也可以是接收器.字段),结构体中方法和字段的命名不能相同,因为调用时会有歧义。方法可以被声明到任意类型,只要不是一个指针或者一个interface。 

  51. 当这个接收器变量本身比较大时,我们就可以用其指针而不是对象来声明方法 。在现实的程序里,一般会约定如果一个类有一个指针作为接收器的方法,那么这个类的所有方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。 此外,为了避免歧义,在声明方法时,如果一个类型名本身是一个指针的话,是不允许其出现在接收器中的 。

  52. 如果接收器p是一个Point类型的变量,并且其方法需要一个Point指针作为接收器,我们可以用下面这种简短的写法:
    p.ScaleBy(2) //ScaleBy函数的原型:func (p *Point) ScaleBy(factor float64)
    编译器会隐式地帮我们用&p去调用ScaleBy这个方法。我们不能通过一个无法取到地址的接收器来调用指针方法,比如临时变量的内存地址就无法获取得到:
    Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
    但是我们可以用一个 *Point 这样的接收器来调用Point的方法,因为我们可以通过地址来找到这个变量,只要用解引用符号* 来取到该变量即可。编译器在这里也会给我们隐式地插入* 这个操作符,所以下面这两种写法等价的:
    pptr.Distance(q) //Distance函数的原型是:func (p Point) Distance(q Point) float64   
    (*pptr).Distance(q) 

  53. a) 不管你的method的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。b)在声明一个method的receiver该是指针还是非指针类型时,你需要考虑两方面的内部,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果你用指针类型作为receiver,那么你一定要注意,这种指针类型指向的始终是同一块内存地址,就算你对其进行了拷贝。 

  54. 调用方法可以通过方法值和方法表达式两种,p.Distance叫作“选择器”,选择器会返回一个方法"值"即一个将方法(Point.Distance)绑定到特定接收器变量的函数。 如:

    p := Point{1, 2}
    q := Point{4, 6}
    distanceFromP := p.Distance// method value
    fmt.Println(distanceFromP(q)) // "5" 直接传入参数就可以
    当T是一个类型时,方法表达式可能会写作T.f或者(*T).f,会返回一个函数"值",这种函数会将其第一个参数用作接收器,所以可以用通常(译注:不写选择器)的方式来对其进行调用:
    p := Point{1, 2}
    q := Point{4, 6}
    distance := Point.Distance// method expression
    fmt.Println(distance(p, q)) // "5"
    fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
    scale := (*Point).ScaleBy
    scale(&p,2)
    fmt.Println(p)// "{2 4}"
    fmt.Printf("%T\n", scale) // "func(*Point, float64)" 
  55. Go语言里的集合一般会用map[T]bool这种形式来表示,T代表元素类型 。在数据流分析领域,集合元素通常是一个非负整数,集合会包含很多元素,并且集合会经常进行并集、交集操作,这种情况下,bit数组会比map表现更加理想。 bit数组:http://blackbeans.iteye.com/blog/1812663

    https://blog.csdn.net/qq_37375427/article/details/79797359
  56. 接口类型是对其它类型行为的抽象和概括;一个实现了这些方法的具体类型是这个接口类型的实例。因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力。 

  57. interface{}类型(空接口类型),它没有任何方法。因为空接口类型对实现它的类型没有要求,所以我们可以将任意一个值赋给空接口类型。 

  58. 接口类型的相关概念:http://www.cnblogs.com/susufufu/p/7353312.html

  59. 接口赋值:https://studygolang.com/articles/5788

  60. 接口方法的传值和传指针的注意:https://www.linuxidc.com/Linux/2017-05/143413.htm
     
     

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值