Go语言学习笔记 | 青训营

132 阅读9分钟

基本变量声明和函数声明方式

package main
import (
    "fmt"
    "log"
)

func main() {
    var a int
    b := 0
    _, err := fmt.Scanf("%d %d", &a, &b)
    if err != nil {
        log.Fatal("Bad Input")
    } else {
        fmt.Println("Good")
        fmt.Println("Result is", sum(a, b))
    }
}

func sum(a, b int) int {
    return a+b
}

package是管理代码的方式,类似于module或者库。一般一个目录作为一个包,程序入口在main包中的main函数。

import导入包,在代码中用到没导入的包ide会自动导入,不允许导入了包却不使用。
可以使用import cdd "fmt"起别名防止名称冲突。

  • 函数体内部声明变量的两种方式,var 和 :=, 自动推导类型。
  • 全局变量只能用var声明,因为格式要求必须由这些单词开头。
  • 可以多个变量同时赋值,多个变量赋值时,只要有一个变量是新的就可以使用:=,但不能改变重复变量的类型,一般多次出现的是err
  • 不允许变量声明而未使用,类似于import的包。
  • 使用_接收但不使用变量。
  • nil为空值。
  • if和for后面条件不需要括号。
  • 函数定义格式是函数名 参数名 返回值类型。
  • 函数可以返回多个值,函数参数类型一致的时候可以将类型写在后面。
  • 大写字母开头就是包外可见,小写就是私有。
  • defer在函数返回前调用,多个defer是后进先出,类似栈。

带有初始化的if语句:

if err := process(); err != nil {
    return err
}

初始化的变量不能在if语句体外使用,但在else if和else中能使用。

switch case语句

与cpp的区别是不需要break。 switch后面不跟变量,则变为if-else的另一种形式。
类型开关

    whatAmI := func(i interface{}) {
        switch t := i.(type) {
        case bool:
            fmt.Println("I'm a bool")
        case int:
            fmt.Println("I'm an int")
        default:
            fmt.Printf("Don't know type %T\n", t)
        }
    }
    whatAmI(true)
    whatAmI(1)
    whatAmI("hey")

选择一个接口的类型。

可变参数函数

调用的...表示解包
接收任意数量的int参数:

func sum(nums ...int) {
    fmt.Print(nums, " ")
    total := 0
    for _, num := range nums {
        total += num
    }
    fmt.Println(total)
}
func main() {

    sum(1, 2)
    sum(1, 2, 3)

    nums := []int{1, 2, 3, 4}
    sum(nums...)
}

结构体

定义结构体与声明结构体变量:

type Se struct {
    Name string
    Power int
}
// 结尾的逗号是必须的,保持一致,有利于版本控制提交
gg := Se {
    Name:"go",
    Power:66,
}

可以不给定所有值,没有给的会默认初始化,也可以不写字段名,按顺序初始化结构体。
使用指针实现引用传递,可以改变结构体的值,减少copy值。
结构体和接口嵌入可以表达一种组合关系。

type base struct {
    num int
}
func (c base) describe() string {
    // code here
}
// container 包含base
type container struct {
    base
    str string
}

func main() {
// 初始化container,给定base
    c := container {
        base: base{
            num:1
        },
        str: "your name",
    }
// 可以通过container直接访问base的属性和方法
// c.num c.base.num 都可以 c.describe()
    type describer interface {
        describe() string
    }
    // 可以将container赋值给接口
    var d describer = c
    d.describe()
}

方法

将函数关联到结构体上。

func (s *Se) Super() {
    s.Power += 1000
}
gg.Super() // 1066

数组、切片、map

数组声明后长度固定。
切片slices是数组的view,定义是不在方括号内写入数值:scores := []int {1, 4, 51, 6}
使用make进行创建:scores := make([]int, 0, 10)创建一个int类型的,长度为0,容量为10的slice。容量的意思是数组的大小,而长度是目前这个slice的view大小。
使用索引调整长度:scores = scores[0:8],长度就调整为了8,但不能超过容量。
append是特别的,如果增加元素后超过了原来的长度,会进行找一块更大的内存复制并重新创建数组,按照2倍扩大容量。例如一个长度为5的数组,append到25需要3次扩大,10,20,40,最终是40的容量。
GO语言的切片不是复制数组,而是操作数组的一个view,会改变数组的值,并且不支持负数索引。
使用切片调用内建函数copy可以使用两个参数完成复制。
性能优化:

  • 给定size直接一次开辟空间,减少从0开始append导致的多次复制操作,类似于cpp的STL中vector优化。
  • 使用copy从大数组中复制,而不是re-slice大数组,这样大数组会存在引用,不会被及时的回收,占用空间。
// 复制的结果是worst从索引2开始复制scores的第一个元素,多了不覆盖,少了也不会补
worst := make([]int, 5)
copy(worst[2:4], scores[:1])

map就是hash或者字典。使用make可以创建。

	lookup := make(map[string]int)
	lookup["g"] = 1004
	p, exist := lookup["vege"]
	fmt.Println(p, exist) // 0 false

len获取长度,delete(lookup, "g")删除kv。给make传递第二个参数可以指定大小。另一种初始化map的方式是直接给值。可以使用for range遍历map,无顺序。
性能优化:
在可以预估大小时,给定大小减少扩容操作。

接口interface

参考资料:go interface使用
定义了合约但没实现:

type Logger interface {
    Log(message string)
}

使用起来和其他类型一样,不需要显示指定,只要一个结构体有一个函数名字叫Log,而且参数是一个string并且没有返回值,就会绑定到Logger这个接口上。
之后就可以传递给Logger类型的值,动态的类型确定。
接口是一种类型,也是方法的集合。它关注的是抽象集能做什么。 特别的是,空接口:type s interface{}.如果函数接收参数为空接口,实际上可以传入任意类型的数据,因为所有类型的数据都满足,传入后发生类型转换,转为interface{}类型,一共分为两部分,一部分是方法表,一部分是实际的数据。

错误处理

使用返回值进行错误处理,可以自定义错误类型,实现接口error:

type error interface {
    Error() string
}

通过errors包,可以使用errors.New("error message")生成一个error类型。
也可以定义自己的error,只要实现Error()方法。

type arrError struct {
    arg int 
    prob string
}
func (e *arrError) Error() string {
    return fmt.Sprintf("%d - %s", e.arg, e.prob)
}
// 返回自己定义的err类型,返回结构体的&,和new的效果一致
func test(arg int) (int, error) {
    if arg == 22 {
        return -1, &arrError{arg, "can't do this with arg"}
    }
    return arg, nil
}
// 通过类型断言得到自定义错误类型的实例
func main() {
    _, e := test(22)
    if ae, ok := e.(*arrError); ok {
        fmt.Println(ae.arg)
        fmt.Println(ae.prob)
    }
}

并发

协程

协程是轻量级的线程。使用go关键字执行函数即可创建一个协程,如果只想执行一次,可以使用匿名函数。创建协程不会阻塞,如果主进程退出,那么可能等不到协程执行完毕。

通道

一个协程可以通过一个通道向另一个协程发送数据,使用make可以创建通道。
c := make(chan int)这个通道的类型是chan int。支持的操作有接收和发送。

// 向通道发送
channel <- Data
// 从通道获取
VAR :=  <- channel

这两个操作是阻塞的。
使用make创建通道时,给定第二个参数可以设置缓冲区大小。
无缓冲的通道是阻塞的,发送会阻塞到其他协程接收,接收也会阻塞。
设置缓冲区后可以在发送时不阻塞,直到缓冲区满才阻塞。接收操作也是类似的。 通道可以设置只读或者只写,<-chan表示只读,chan<-表示只写。
通道可以使用close()关闭,关闭之后不能再往通道里写,但还可以读。
读的时候,使用两个接收值,第二个是判断通道内还有没有数据。
使用for range可以遍历通道,得到所有的值。通道发送完必须要关闭,否则会一直等待通道关闭。

select通道选择器

select case 结构,可以同时等待多个通道。
配合time包的After实现超时处理:

import (
    "fmt"
    "time"
)
func main() {
    c1 := make(chan string, 1)
    go func() {
        time.Sleep(2 * time.Second)
        c1 <- "result 1"
    }()
    select {
    case result := <-c1 :
        fmt.Println(result)
    case <-time.After(1 * time.Second):
        fmt.Println("timeout")
    }
}

如果在select体中添加default字段,那么就成为了非阻塞的。如果检查通道都不满足,会直接执行default中的语句。

文件读写

读文件

dat, err := os.Readfile("/path")
读取到内存中fmt.Print(str(dat))转为字符串输出。
f, err := os.Open("/path")打开一个文件,然后通过f操作文件。

    b1 := make([]byte, 5)
    n1, err := f.Read(b1)
    fmt.Printf("%d bytes: %s\n", n1, string(b1[:n1]))

从文件的开始读取,最多读取5个字节,n1是实际读取到的字节数。
Seek可以定位到文件的某个位置,然后从这个位置开始操作。

    o2, err := f.Seek(6, 0)
    b2 := make([]byte, 2)
    n2, err := f.Read(b2)

Seek的两个参数,第一个是位移量,可以是负数,表示向前移动字节数,第二个参数是如何操作,0就是从文件开始,1是从当前文件指针位置,2是从文件末尾开始。
通过bufio包提升读取文件的效率。
在使用open打开文件得到os.File对象后,将其传入bufio.NewReader()中得到一个缓冲区,当缓冲区满时才会读文件,将碎片化的小读写整合。r.ReadString()读取一行,直到错误或者EOF。
在读取文件之后,要接上defer f.Close()关闭文件对象。

写文件

os.WriteFile("/path", b1, 0644),文件路径,写入内容,和文件权限。
更细致的操作,先Create()一个文件,然后通过f对象Write内容,f.Sync()刷新缓冲,写入磁盘。
同样的,bufio包也有NewWriter()可以使用。使用w.Flush()刷新缓冲。

命令行参数

使用flag库可以实现解析命令行参数,实现CLI应用。

func main() {
	var name string
	flag.StringVar(&name, "name", "GO语言学习", "帮助信息")
	flag.StringVar(&name, "n", "GO语言学习", "帮助信息")
	flag.Parse()
	log.Printf("name: %s", name)
}

使用StringVar绑定了字符串参数到name和n上,一个长参数和一个短参数,都会写到name这个变量上。第三个参数是默认值,也就是不给的时候的信息。第四个是帮助信息,使用-help--help可以查看所有的参数作用。

image.png