[Tour of Go] Golang基础 | 青训营笔记

140 阅读12分钟

Golang基础

看官网文档做的笔记。厌倦了每次捡起Go都要重看文档了。

官网的 Tutorials 大部分是一些使用Go进行开发的简单流程示例,我个人感觉是,按照按需自取的原则,稍微看一下,敲一下,理解了流程就好。 基础语法可以通过 Tour of Go 学习,然后看 Effective Go进阶。这里是记录 Tour of Go的笔记。 @[TOC](Table of Contents)

Hello World

package main
import "fmt"
func main() {
    fmt.Println("Hello, World!")
}

第一章 包,变量,函数

每个Go程序都是由 package (下称包)构成的,程序的运行起点都在 package main 中 package名与引入路径的最后一个元素相同,例如 import "math/rand",引入了首行声明为 package rand 的文件

导出命名(Exported names),在Go中,大写字母开头的命名(包括常量变量函数结构)会被导出,你只可以使用引入包的导出命名,任何未导出命名在包外部不可达

函数参数可以有0或多个,参数声明时类型在变量名之后 argname type 多个参数类型相同时,可以只保留后面的类型声明func f(x,y,z int) int 函数可以返回任意数量的结果 函数返回值可以命名 func split(sum int) (x,y,int) ,相当于在函数开始定义的变量,return语句没有返回值时,会使用函数中返回值的命名对应变量返回。返回值命名应当是语义明确的。为避免影响可读性,返回值命名应当只用在短函数中。

package main
import "fmt"
func split(sum int) (x, y int) {
	x = sum * 4 / 9
	y = sum - x
	return sum,sum
}
func main() {
	fmt.Println(split(17))
}

var关键字用来定义变量,与函数参数类似,变量名在前,类型在后,相同类型可只保留最后。 var定义可以位于包级(非文件级)或函数级中 var声明变量时可以进行初始化,如果初始值被给定,类型可以忽略,编译器自动推导变量类型(好像是懒推导,到了用的时候再推导,不同用的地方推导出的类型就可能不一样,主要指数值型)

package main
import "fmt"
var i, j int = 1, 2
func main() {
	var c, python, java = true, false, "no!"
	fmt.Println(i, j, c, python, java)
}

函数内部,可以使用 :=海象运算符取代var进行隐式类型的变量声明 k:=3 类型推导时,数值型变量会根据精度推导为int``float64``complex128函数外,每条语句都以关键字开头,所以不能使用海象运算符

Go的基本类型有:

  1. bool
  2. string
  3. int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,uintptr
  4. byte (uint8的别名)
  5. rune (int32的别名,代表一个Unicode字符编码,单引号)
  6. float32,float64
  7. complex64,complex128 这里展示了变量声明也可以像import引入一样被包含进“块” 如果没有特殊需求,整数值使用int即可
import (
	"fmt"
	"math/cmplx"
)
var (
	ToBe   bool       = false
	MaxInt uint64     = 1<<64 - 1
	z      complex128 = cmplx.Sqrt(-5 + 12i)
)

未初始化的变量会被给予零值 对于数值类,零值为0; 对于布尔类,零值为false 对于字符类,零值为"",即空字符串

使用T(v)进行类型转换,将v转换为类型T,注意与Clang不同的是,类型转换必须去显式调用,不会自动转换

常量必须使用const关键字生成,可以是基本类型中的一种,支持类型推导 数值常量是高精度数值,在不指定类型时,会根据上下文推导,就是类似,到了用的时候再给你推导

关于引号,单引号表示单字符,是rune类型的字面量,双引号表示string的字面量,可以是单字符或字符串,反引号表示原生字符,内容不会被转义,可以是多行

第二章 流控制语句

只有一个for循环,用法类似其他语言,但不需要括号,但花括号是必须的(可能就是指循环语句只有一行也要加) for循环的三条语句里可省略初始化语句和后处理语句,只需要条件语句,这样就类似while了,所以可以不加分号 条件语句再没有了,就是无限循环了

package main
import "fmt"
func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	//
	sum2 := 1
	for sum2 < 1000 {
		sum2 += sum2
	}
	//
	for {
	}
	fmt.Println(sum,sum2)
}

判断语句类似,没括号,但花括号必须有 可以在条件语句前加一条语句执行,语句中的变量在if作用域(包括else)中都可用(异常处理好用的)

func sqrt(x float64) string {
	if x < 0 {
		return sqrt(-x) + "i"
	}
	return fmt.Sprint(math.Sqrt(x))
}
func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {
		return v
	} else {
		fmt.Printf("%g >= %g\n", v, lim)
	}
	return lim
}

switch case 语句被视为if``else语句的序列式表达,运行第一个符合条件语句的case 与别的语言不同点,只运行符合条件的,不会执行之后的,效果上相当于Go会给你在每个Case 后面加break语句(可以使用fallthrough语句让其无条件继续向下执行)。case的条件不必须是常量, 也不一定是数值。(就是可以是条件语句,switch关键字后面是值初始化语句分号待判断值(省略就直接是前面初始化的值),拿待判断值看它是否符合case的条件语句,值的话应该是直接对比,语句是判断) 自顶向下执行计算匹配 没有条件的switch,用这种结构来写很长的if-then-else本质是用true去匹配条件

func main() {
  switch num := time.Now().Month(); {
  case num <= 3:
    fmt.Println("当前是第一季度")
    fallthrough
  case num > 6:
    fmt.Println("当前是下半年")
  default:
    fmt.Println("未知月份")
  }
}
func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		fmt.Printf("%s.\n", os)
	}
}
func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}

defer 关键字可以将语句延迟至其所在函数返回后再执行,说的很含糊,建议参考下文 大概就是return分两步,defer在这两步之间进行,所以如果是使用命名返回值,在defer里对返回值的更改会生效,如果是直接返回,实际上Go会生成一个没名字的变量先存了结果,defer执行,然后返回没名字结果,所以defer访问不到,修改不了 defer解析 多个defer语句会被压入栈中,后出现的语句先执行

第三章 结构,切片,映射

指针,*T就是指向T类型的指针类型,指针的空值为nil 其实只要理解,*pointer取pointer的内容,&value取value的地址 取内容也称“解引用”(dereference),取地址也称“间接取值”(indirecting) Go没有对指针的算术运算

结构,是一系列域的集,类似Clang里的结构体 通过 . 运算符获取结构里的值 对于结构的指针,不需要显式 解引用 (*p).field来获取值,可以直接 p.field

结构初始化赋值时,或者按序传入域的值,或者显式指定域赋值,未赋值域为零值,再或者不赋值,域默认是零值

[n]T 表示 由n个T类型的值组成的数组,因为长度是其类型的一部分,所以不能改变长度

[]T 表示 T类型的切片,切片由两个索引给定,类似Python数组的切片,半开区间,取前不去后,索引缺省为0和n+1 视为对数组的引用,不存储数据,只是对依赖数组的引用,改变都会改变原数组,共享数组的多个切片都会改变。

切片具有长度和容量,可以用 len(s) cap(s)全局方法获取,长度表示具有的元素个数,容量表示从==其==第一个元素开始数,到==其依赖数组或切片==元素末尾的个数(注意这个定义,重切片时容量可能会变,按照这个定义去算就OK) 切片的空值为nil, 空切片长度容量都为0,无依赖数组,零切片仅长度为0,容量来自上级切片或数组,有依赖数组 就是说,切片的长度取决于两个索引,容量取决于上一级切片或数组,不是最原始的数组 切片也可以直接被创建如下2,但是本质是建立一个如下1的数组,然后再全部切片,所以len=3,cap=3,但是后续添加元素会新建合适长度的依赖数组,所以当动态数组直接用吧。注意扩充以后不再依赖原数组所以对切片也不能改变原数组

[3] int {1,2,3} [] int {1,2,3}

使用make创建切片,指定一个参数为len,cap默认等于len,且值为0,两个参数为len,cap

a := make([]int, 5)  // len(a)=5, cap(a)=5
// a: [0,0,0,0,0]
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
// b: []

切片中可以包含任何类型,包括切片


func main() {
	// Create a tic-tac-toe board.
	board := [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}

	// The players take turns.
	board[0][0] = "X"
	board[2][2] = "O"
	board[1][2] = "X"
	board[1][0] = "O"
	board[0][2] = "X"

	for i := 0; i < len(board); i++ {
		fmt.Printf("%s\n", strings.Join(board[i], " "))
	}
}

使用append方法为切片添加元素,注意添加元素超过cap之后会自动扩充,但是会丢失对上级切片或数组的引用。

func append(s []T, vs ...T) []T
s = append(s,0,1,2,3)

rangefor配合使用来迭代切片或者映射,会类似Python里的enumerate函数,返回索引和值,可使用 _忽略不需要的值,只要一个的话是索引

for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
}
for i, _ := range pow
for _, value := range pow
for i := range pow

映射,一系列键值对 映射的零值为 nil, 零映射没有键,也不能添加键 使用 make创建映射 给定键和值以在初始化时赋初值,或者之后map[key] = value赋值

type Vertex struct {
	Lat, Long float64
}
var m map[string]Vertex
var n = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}
func main() {
	m = make(map[string]Vertex)
	m["Bell Labs"] = Vertex{
		40.68433, -74.39967,
	}
	fmt.Println(m["Bell Labs"])
}

赋值,直接获取值,删除值,判断值是否存在(ok是一个bool值,true则键存在,elem为m[key],false反之,elem为key类型对应的零值),往往直接用海象运算符来操作

m[key] = elem
elem = m[key]
delete(m,key)
elem,ok = m[key]
elem,ok := m[key]

函数也是值,也有类型,也可以做函数参数和返回值

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

函数闭包,可以让变量常驻内存,并且不污染全局命名 如下中的adder,作用域会常驻内存,对于每一个作用域,sum变量都不被销毁 主要目的是为了全局变量滥用(?)

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}
func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}
package main
import "fmt"
// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
	a1 := 0
	a2 := 1
	return func() int {
		c := a1
		a1=a2
		a2=a2+c
		return c
	}
}
func main() {
	f := fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}

第四章 方法与接口

Go没有类(?),所以需要在结构上定义方法,方法是带有“接受者”的函数,用来给结构绑定方法 在 func关键字和方法名之间指定接收者(类型),也可以放到参数列表里,但那样不能用类似instance.method()的形式调用

type Vertex struct {
	X, Y float64
}
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func Abs(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
}

可以在非结构类型上定义方法,比如自己用type给内置类型定义个别名,然后就可以给她们绑定方法 只能给在当前包内定义的类型绑定方法,所以也不能直接给内部类型绑定方法

type MyFloat float64
func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}
func main() {
	f := MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}

但是,上面那样绑定的方法,因为相当于传值调用,并不能改变实例的值,考虑到类的方法往往会修改实例的值,所以应该按实例地址绑定,每次调用修改实例的属性 然后你把他视为函数的话,参数v就应该是个指针,但是Go会给你把 v.Scale(10)解释为 (&v).Scale(10) 就是说Go为了方便会给你自动解引用或者取地址,结构的指针可以直接访问域也是一样 p.field->(*p).field

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}
func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.Abs())
}

关于值接收器和指针接收器的选择:对给定类型,应当只使用其中一种,并且往往只使用指针接收器,一是能修改实例值(即使函数不修改值),二是避免每次调用都把结构复制一遍 还要注意的是,这两种绑定直观上是绑定在同一个“类”上,但实际上值接收器绑定在原类型,指针接收器绑定在原类型的指针类型上

接口类型,一系列方法签名的集合 接口类型的变量可以被赋值为被任何实现了她内部所有方法的类型的值 实现了接口类型内部的方法就实现了这个接口,无需显式的implements关键字来声明

隐式接口将接口定义与接口实现解耦,接口实现可以无需约定出现在任何包中

大概就是,实现接口没必要刻意在意实现了哪些,用的时候编译器会知道 给我看不懂了,参考以下:


接口本身是调用方和实现方均需要遵守的一种协议,大家按照统一的方法命名参数类型和数量来协调逻辑处理的过程。 Go 语言中使用组合实现对象特性的描述。对象的内部使用结构体内嵌组合对象应该具有的特性,对外通过接口暴露能使用的特性。 Go 语言的接口设计是非侵入式的,接口编写者无须知道接口被哪些类型实现。而接口实现者只需知道实现的是什么样子的接口,但无须指明实现哪一个接口。编译器知道最终编译时使用哪个类型实现哪个接口,或者接口应该由谁来实现。


接口类型的值(下称接口值),可以被视为一个元组,包括当前所依赖类型的一个实例(下称依赖值)及其类型,所以接口类型的值调用方法会执行所依赖类型的同名方法 接口值也可以像变量一样做函数参数和返回值,赋值传递等

接口值可以没有依赖值,就是,比如给接口值赋一个刚初始化的实现该接口的类型值,调用方法时就没有接收者,别的语言可能会触发空指针异常,对于Go,往往会书写优雅地处理空指针接收者的方法

type T struct {
	S string
}
func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

依赖变量为 nil的接口值本身不是 nil

空接口值为,依赖值为空,依赖类型为空,如果调用空接口的方法会runtime error

空接口类型,没有指定方法的接口类型,所有类型都至少实现了空接口,(所以Go里每个值都至少具有依赖类型和依赖值?都可以赋给空接口,每个接口又都具有依赖类型和依赖值),往往被用来处理未知类型

类型断言,获取接口值的依赖值 通过判断接口依赖类型是否实现了给定接口或是否为给定类型,实现则返回给定类型的依赖值和状态true,否则返回零值和false

var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // panic
fmt.Println(f)

通过switch case进行类型断言

switch v := i.(type) {
case T:
    // here v has type T
case S:
    // here v has type S
default:
    // no match; here v has the same type as i
}

所以最常见的接口之一如下,转为字符串,打印变量时都会断言一下实现了没,实现了就打印返回值

type Stringer interface {
    String() string
}

所以,不建议在一个“类”上使用两种绑定方式,是觉得加入接口的使用后会导致混乱吗?

异常,error类型,空值为nil,实际上是一个内置接口,函数通常返回异常值,调用时应该测试异常是否为nil并处理异常 异常值为nil表示无异常,反之则应处理异常

i, err := strconv.Atoi("42")
if err != nil {
    fmt.Printf("couldn't convert number: %v\n", err)
    return
}
fmt.Println("Converted integer:", i)

函数返回异常的写法

注意: 在 Error 方法内调用 fmt.Sprint(e) 会让程序陷入死循环。可以通过先转换 e 来避免这个问题:fmt.Sprint(float64(e))。这是为什么呢?

四循环原因是,e的类型实现了异常接口,sprintf会将之视为异常类型寻找Error方法打印,然后我们写的Error里又调用sprintf,然后就死循环了,但是float64类型因为不是异常,所以没有实现异常接口,就可以用视为stringer接口用string方法打印

type ErrNegativeSqrt float64
func (e ErrNegativeSqrt) Error() string{
	return fmt.Sprintf("cannot Sqrt negative number: %v", float64(e))
}
func Sqrt(x float64) (float64, error) {
	res := float64(0)
	var err error
	if x<0{
		res = 0
		err = ErrNegativeSqrt(x)
	}else{
		res = math.Sqrt(x)
		err = nil
	}
	return res, err
}
func main() {
	fmt.Println(Sqrt(2))
	fmt.Println(Sqrt(-2))
}

IO,io.Reader接口,包括read方法,实现该接口来读取数据流,标准库有很多实现 一种常见模式是用一个reader包装一个reader来修改流 比如下面这里,仿佛是对b进行反复擦写,然后从 b里获取reader里的数据,直到err==io.EOF

func (T) Read(b []byte) (n int, err error)

示例

package main
import (
	"fmt"
	"io"
	"strings"
)
// 总是返回一字节的,元素只有 'A'的切片
func (mrd MyReader) Read(b []byte) (int,error) {
	// 注意这里是对单字符赋值,应该用单引号 rune类型的字面量
	b[0]='A'
	return 1,nil
}
func main() {
	r := strings.NewReader("Hello, Reader!")
	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}

图像IO,image.Image接口,实现如下方法

package image
type Image interface {
    ColorModel() color.Model
    Bounds() Rectangle
    At(x, y int) color.Color
}

第五章 并发

(白:学操作系统时我在做什么啊!!!)

Goroutines,一个 goroutine 就是一个由Go的内存管理机制管理的轻量线程(下称grt) 使用 go func(args...)开启一个grt,函数参数计算仍在当前grt,比如fo f(x,y,z),这里的参数 x, y, z 的计算还是在当前grt进行,但是 f 的执行就会在新grt 中 grts运行在相同的命名空间,所以获取共享的内存必须是同步的,sync包提供了有用的原语,但是不常用

Channels,是一种 管道,可以通过管道运算符<- 来发送和接收某类型的值(所以管道需要类型),箭头所指方向就是数据传递方向(下称chan)

ch <- v    // Send v to channel ch.
v := <-ch  // Receive from ch, and
           // assign value to v.

使用make创建chan,需指定传递的数据类型(chan int类型)

ch := make(chan int)

通常,发送和接收数据会等待另一方准备好,所以使用 grts 无需通常并发编程中的 显式锁 或 条件变量 示例

package main
import "fmt"
func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // send sum to c
}
func main() {
	s := []int{7, 2, 8, -9, 4, 0}
	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // receive from c
	fmt.Println(x, y, x+y)
}

Buffered Channels,缓存管道,创建chan时指定第二个参数,即缓存大小,chan中能缓存的元素个数 缓存区满时管道接收堵塞,缓存区空时接收管道值堵塞,会报错 结构上类似队列,先进先出

ch := make(chan int, 100)

管道可以被关闭,通过close(chan)全局方法关闭管道,注意只有发送方可以关闭管道 可以在接收值时获取管道状态,false为关闭状态

v,ok := <-ch

chan并不像文件,关闭不是必须的,只有在接收方需要被告知没有值传递时才是必须的,比如打断range对缓存管道的读取时 可以使用range对缓存管道进行读取,此时发送方须关闭管道

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}
func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

使用select case 来让一个 grt 等待多个传递操作,直到某个case可以被执行,多个都可执行时随机选一个 示例

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}
func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

没有case准备好时就执行 default区块

func main() {
	tick := time.Tick(100 * time.Millisecond)
	boom := time.After(500 * time.Millisecond)
	for {
		select {
		case <-tick:
			fmt.Println("tick.")
		case <-boom:
			fmt.Println("BOOM!")
			return
		default:
			fmt.Println("    .")
			time.Sleep(50 * time.Millisecond)
		}
	}
}

如果不需要传输数据,只是想在避免冲突的情况下,确保每次只有一个grt获取到同一个变量, 即达到 互斥 的效果,Go使用 sync.Mutex结构提供这种功能,包括两个方法:

Lock
Unlock

示例

package main
import (
	"fmt"
	"sync"
	"time"
)
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
	mu sync.Mutex
	v  map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
	c.mu.Lock()
	// Lock so only one goroutine at a time can access the map c.v.
	c.v[key]++
	c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
	c.mu.Lock()
	// Lock so only one goroutine at a time can access the map c.v.
	defer c.mu.Unlock()
	return c.v[key]
}
func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}
	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
}