Go语言编程 (1)

《Go语言编程》阅读笔记(1) 基本的语法和语言特点

初识Go

  • 静态语言

  • 语言特性:

    • 自动垃圾回收

    • 内置类型丰富

    • 多返回值的函数

    • 错误处理:关键字defer、panic、recover

    • 匿名函数、闭包——函数可作为参数传递

      1
      2
      3
      f := func(x, y int) int {
      return x+y
      }
    • 并发编程:每个用关键字go执行的函数可以运行成为一个单位协程,一个协程阻塞,调度器把其他协程安排到另外的线程执行;用通信顺序进程CSP作为goroutine间的通信方式,用channel实现CSP(类似管道pipe)

    • 类和接口

    • 反射:获取对象类型的详细信息,需要import reflect

  • 通过Cgo重用现有的C模块,利用Cgo的特定语法混合C代码

  • GOPATH和PATH环境变量类似,能接受多个路径

  • 源码开头为package声明,表明该源码所在的包,包是基本的分发单位;生成Go的exe,必须建立名为main的包,其中包含main函数,main()无参数,无返回值,命令行参数保存在os.Args,flag包用于规范命令行参数并获取、解析命令行参数

  • 函数:返回值没有明确赋值时将被设置为默认值

    1
    2
    3
    func 函数名(参数列表)(返回值列表){
    //参数1 参数1数据类型, 参数2 参数2数据类型, ...
    }
  • 注释格式同C++

  • 左边花括号不允许另起一行

  • 问题追踪:

    • fmt包提供打印函数,其中Printf与PrintIn()类似于C的Printf()

      image-20220418210923875
    • log包:日志功能

    • go内置了print函数,但属于输出到标准错误流中并打印

顺序编程

变量

  • 变量声明:关键字var,变量类型在变量名后;若干声明的变量可以放置在一起

    image-20220419113631944
1
2
3
4
var (
v1 int
v2 string
)
  • 变量初始化:可以删去var,var v1 int = 10var v2 = 10v3 := 10是相同的,符号:=表示变量声明和初始化同时进行,此时左边的变量不能是被声明过的

  • go的变量声明后必须使用

  • 可以有i,j=j,i

  • 函数可以有多个返回:

    image-20220419114128955

常量

  • 字面常量:无类型,只要在相应类型的值域里就可以作为该类型的常量,如-12可以赋值int、uint、int32、float64、complex64等类型的变量

  • 可以通过const关键字定义,可以不限定常量类型

    image-20220419114502144
  • 三个预定义常量:true、false、iota;iota在每一个关键字出现时被重置为0,下一个const出现前每出现一次iota,其对应数字自动加一

    image-20220419114629960
  • 枚举一系列的常量

    1
    2
    3
    4
    const (
    Friday = iota
    number_of_days
    )
  • 大写字母的开头的常量在包外可见

数据类型

  • bool:var v1 := (1==2),不接受其他类型赋值,不支持自动和强制类型转换

  • 整型:int和int32是不同类型,需要强制转换value2 = int32(value1)(存在精度损失、值溢出的问题)

    image-20220419122228025

  • 支持+-*/和%运算,支持比较运算(同C),不同类型的整型数不能直接比较,会编译报错,但可以直接和字面常量比较

    1
    2
    3
    4
    5
    var i int32 = 1
    var j int64 = 2
    if i == 1 || j == 2 {
    ...
    }
  • 位运算和C类似,取反为^x

  • float64相当于double,value1 := 12.0,浮点数的比较建议使用:

    1
    2
    3
    4
    5
    import "math"
    // p 为自定义的比较精度,如0.0001
    func IsEqual(f1, f2, p float64){
    return math.Fdim(f1, f2) < p
    }
  • 复数:var value1 complex64 = 3.2 + 12i——两个float32构成,通过real()imag()获得实部和虚部

  • 字符串:var a string = "abc",通过数组下标获取内容,不能在初始化后修改,通过len()获取长度,字符串连接用运算符+

  • 支持两个字符类型:byte和rune(代表单个Unicode字符),go的多数api假设字符串为utf-8编码

  • 数组声明方法:

    image-20220419125939080 * 遍历:`for i:=0; i< len(array); i++ {}`或`for i, v := range array {}`——range:索引+元素值,类似enumerate
    • go中数组为值类型(value type),所有值类型变量赋值和参数传递时产生一次复制——数组作为函数参数时,会复制一次,函数体无法修改外面数组内容

    • 修改方法:数组切片,数组切片分为三个变量,一个指向原生数组的指针,数组切片中元素个数,数组切片已分配的存储空间;数组切片仍使用数组管理元素(类似C++中vector和数组的关系)

      • 根据一个数组创建数组切片:

        image-20220419130448384
      • 根据make()创建数组切片:(make只能为slice, map, channel分配内存)

        image-20220419130628691
      • 操作数组的所有方法都适用于数组切片

      • cap()返回数组切片分配空间大小,len()返回数组切片当前元素数目

      • append()slice = append(slice, 1, 2, 3),尾部加上3个元素,生成一个新的数组切片;也可slice = append(slice, slice2...)slice2后面有3个点,不能省略

      • 基于数组切片创建数组切片,只要选择范围不超过cap(oldslice)即可,多余位置补0

        1
        2
        oldslice := []int {1,2,3}
        newslice := oldslice[:5]
      • copy(slice1, slice2):按较短的数组切片元素个数进行复制

  • map:类似dict,键值对的未排序组合

    image-20220419131942255
    • 其中的string为键的类型,后面PersonInfo为值类型
    • 通过make创建一个新的map,可以创建时指定其初始存储能力:cur_map := make(map[string] int, 100),或者创建并初始化一个map:tmp_map = map[string] int{"abc": 1}
    • 通过delete(cur_map, key1)删除键为key1的键值对,若不存在也不报错

流程控制

  • 条件语句:类似C,不允许将最终的return语句包含在if else结构中,花括号必须存在

  • 选择语句:不需要break明确跳出一个case,只有在case中添加fallthrough关键字,才会执行下一个case;若不设定switch后的条件表达式,则整个switch类似于多个if else;

    image-20220419134315797 image-20220419134458366
  • 循环语句只有for,用下面的写法替代while;不支持以逗号间隔的多个赋值语句;break、continue类似C,但break可以选择中断哪个循环

    image-20220419134805878image-20220419135020682

    image-20220419134805878image-20220419135020682

  • goto:跳转到本函数内的某个标签

    image-20220419135111578

函数

  • 若参数列表中若干个相邻参数的类型相同,则参数列表中额可以省略前面变量的类型声明

    1
    2
    3
    4
    5
    6
    func Add(a, b int)(ret int, err error) {
    ...
    }
    func Add(a, b int) int {
    //如果只有一个返回值
    }
  • 调用:如果导入过该函数所在的包,则:c := math.Add(1, 2)

  • Go中函数名字大小写体现了该函数的可见性,有时导入了包,但编译器仍会报错无法找到add_xxx函数——大写字母开头的函数才能被其他包使用,此规则同样适用于类和变量的可见性

  • 不定参数:函数传入的参数数目不确定(下例中,参数类型都是int),形如...type格式的类型只能作为参数类型且为最后一个参数,本质上是一个数组切片。如果希望传递任意类型,则为...interface{}

    1
    2
    3
    4
    5
    6
    7
    8
    func tmp1(args ...int) {
    for _, arg := range args {
    fmt.Println(arg)
    }
    }
    func tmp2(format string, args ...interface{}) {
    //...
    }
  • 可以给返回值命名,其值在函数开始时自动初始化为空:func tmp()(n int, err Error){}

  • 匿名函数:不定义函数名,可以直接赋值给一个变量或直接执行;go的匿名函数是一个闭包

    1
    2
    3
    4
    5
    6
    7
    f := func (a, b int, z float64) bool {
    return a*b < int(z)
    }
    fmt.Println(f(1, 2, 3.5))
    func (ch chan int) {
    ch <- ACK
    }(reply_chan) // 花括号直接跟参数列表,表示函数调用

    错误处理

    • error接口:

      • 接口定义为:

        image-20220419145949906
      • 对于函数,如果要返回error,error通常作为多种返回值的最后一个

        image-20220419150046420
      • 举例:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        type PathError struct {
        Op string
        Path string
        Err error
        }
        func (e *PathError) Error() string{
        return e.Op + " " + e.Path + ": " + e.Err.Error()
        }
        func Stat(name string) (fi FileInfo, err error){
        var stat syscall.Stat_t
        err = syscall.Stat(name, &stat)
        if err != nil{
        return nil, &PathError("stat", name, err)
        }

        return fileInfoFromStat(&stat, name), nil
        }
    • defer:例子如下,一个函数可以存在多个defer语句,defer的调用服从先进后出

      image-20220419151534757
    • panic()和recover():报告和处理运行时的错误:

      image-20220419152154859
      • panic()调用时,正常执行流程停止,函数中defer关键字延迟执行的语句开始执行,函数返回到调用它的位置,并逐层向上执行panic流程,直到所属的goroutine中所有在执行的函数终止。报告错误信息,包括调用panic()时传入的参数——此过程称为错误处理流程

      • recover()用于终止错误处理流程,一般应该在一个使用了defer的函数中执行

        image-20220419152702387

实例

  • 结构如下,sorter.go:package main,import "algorithm/bubblesort"import "algorithm/qsort";bubblesort.go:package bubblesort;bubblesort_test.go:单元测试文件,package bubblesort;qsort.go和qsort_test.go类似

    image-20220419154440978
  • flag包解析命令行参数(一些变量可以定义在函数外,作为全局变量,这里*string为指针,类似C++,此时如果要在另一个函数内部修改数组,直接传递指针或数组切片即可)

    image-20220419153343288
  • 文件读取操作,需要导入os包打开文件,file, err = os.Open(infile),文件关闭需要用defer file.close(),然后通过bufio包读取文件内容

  • 文件输出操作,先生成输出文件file, _, _ = os.Create(outfile),之后file.WriteString(string1)

  • strconv包实现基本数据类型与字符串表示的转换,strconv.Itoa(x int) string:接收一个int类型参数,返回x的字符串(反之为Atoi

  • 单元测试文件一般需要导入testing包,一个样例如下

    image-20220419154756827

面向对象

类型系统

  • 可以给任意类型(包括内置类型)添加新的函数,以int为例,定义一个新的类型Integer(就是int)并增加一个新的函数Less(),此时若var a Integer = 1a.Less(2)

    image-20220419160251992 image-20220419160411403
  • Go中没有类似python的self或类似C++的this;上例中,如果要修改对象,可以改用指针:

    image-20220419160634607
  • http包中关于HTTP头部信息的Header类型和上例也类似:

    image-20220419160836856
  • 值语义和引用语义:

    • 对于b=a,如果b的修改不影响a的值,则此类型为值类型。go中大多数类型为值类型,包括:基本类型、数组array(这和C++完全不同)、结构体struct、指针等,若要表达引用,必须:var b = &a
    • 有4个类型具有引用含义:数组切片(指向数组的一个区间)、map、channel、interface
  • 结构体:和其他语言的class具有同等地位,但放弃了很多面向对象特性,只保留了组合特性。例如,定义一个矩形类型,并定义一个成员方法计算矩形的面积(Rect为类的名称,该类是一个struct)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    type Rect struct {
    x, y float64
    width, height float64
    }
    func (r *Rect) Area() float64 {
    return r.width * r.height
    }
    ...
    tmp := &Rect{x: 1, y: 2, width: 2, height: 2}
    fmt.Println(tmp.Area())

初始化

  • 初始化方式如下,没有显式初始化都会被初始化为该类型的零值

    image-20220419162333739
  • go没有构造函数,对象的创建通常由一个全局的创建函数完成(以NewXXX命名):

    image-20220419163716255

匿名组合

  • 以匿名组合的方式实现继承,如下例,定义Base类,有两个成员函数Foo()和Bar(),定义一个Foo类,继承并改写了Bar()方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    type Base struct {
    Name string
    }
    func (base *Base) Foo() {...}
    func (base *Base) Bar() {...}
    type Foo struct {
    Base
    }
    func (foo *Foo) Bar() {
    foo.Base.Bar()
    ...
    }
  • 派生类Foo没有改写即类的成员方法时,相应的方法就被继承,例如foo.Foo()和foo.Base.Foo()效果相同

  • 可以以指针的方式得到“派生类”,此时Foo创建实例时,需要提供一个Base实例的指针——类似于C++的虚基类

    1
    2
    3
    type Foo struct {
    *Base
    }

可见性

  • 没有private、public、protected这些关键字,若某个符号对其他package可见,则该符号以大写字母开头,成员函数的可访问性类似
  • Go语言符号的可访问性是包一级,即包内部的其他类都可以访问它

接口(很重要)

  • go之前,其他语言的接口在实现类时,需要明确声明自己实现了某个接口,即强制性接口继承,称为侵入式接口

  • go中,一个类只要实现了接口要求的所有函数,则称这个类实现了该接口,例如定义一个File类,实现了Read、Write、Seek、Close函数,并定义IFile、IReader、IWriter、ICloser接口。

    image-20220419180023066 image-20220419180104852
    • File没有从这些接口继承,但File实现了这些接口,因此可以进行赋值

      image-20220419180225848
    • 此时不再需要绘制类的继承图,并且实现类的时候只需要关心类提供了什么方法,不需要考虑接口要拆分多细才合理,还不用为了实现一个接口而导入一个包

  • 接口赋值:包括将对象实例赋值给接口,或者将一个接口赋值为另一个接口

    • 前者,要求对象实例实现了接口要求的所有方法,例如,定义一个Integer类型,定义接口LessAdder:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      type Integer int
      func (a Integer) Less(b Integer) bool {
      return a < b
      }
      func (a *Integer) Add(b Integer) {
      *a += b
      }
      type LessAdder interface {
      Less(b Integer) bool
      Add(b Integer)
      }
      var a Integer = 1
      var b LessAdder = &a
      • 此时*Integer既存在Less函数,又存在Add()函数,满足LessAdder接口——Go会根据func (a Integer) Less(b Integer) bool自动生成新的方法func (a *Integer) Less(b Integer) bool {return (*a).Less(b)}
    • 后者,只要两个接口具有相同的方法列表,它们就是等同的,如果接口A的方法列表是接口B的方法列表的子集,则接口B可以赋值给接口A,反之不行

  • 接口查询:查询Writer接口能否转化为IStream接口;进一步可以查询接口只想的对象是否为某个类型(file1接口指向的对象实例是否为*File类型——前文里,接口的赋值使用了&)

    1
    2
    3
    4
    5
    6
    7
    var file1 Writer = ...
    if file5, ok := file1.(IStream); ok {
    ...
    }
    if file6, ok := file1.(*File); ok {
    ...
    }
  • 类型查询:查看接口指向对象实例的类型,例如:

    image-20220419182223072 image-20220419182330415
  • 接口组合:

    image-20220419182509033
  • Any类型:任何对象实例都满足空接口interface{},因此当函数可以接收任意的对象实例时,通常会声明为interface{}(func Println(args ...interface{})

实例

  • go标准库中没有GUI相关的功能,也没有成熟的第三方界面库——go初始定位为高并发的服务器端程序