学习笔记 —— Go 指南
前言:先贴上网址,因为是先用 Typora 写,然后直接导入的,所以格式多多少少有点问题= =
一. 包、变量和函数
-
导包可以用分组形式(括号)
import ( "fmt" "math" )
-
在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。
math.pi // 未导出的变量无法访问 math.Pi
-
函数参数类型在后;连续同类型时可以省略,只在最后一个参数写上类型
func add(x, y int) int { return x + y }
-
多值返回:函数可以返回任意数量的返回值。
func swap(x, y string) string { return y, x }
-
返回值可被命名,位于函数顶部。
无参的 return 语句会直接返回已命名的返回值。(长函数这样使用会影响可读性)
// 已命名返回值 x, y func split(sum int) (x, y int) { x = sum % 10; y = sum / 10; // 会返回 x, y return }
-
var
语句:用于声明一个变量列表,跟函数的参数列表一样,类型在最后。var
语句可以出现在包或函数级别。package main import "fmt" var java, python, golang bool func main(){ var i int fmt.Println(i, java, python, golang) }
-
声明可以包含初始化值
有初始化值时,可以省略类型,会从初始化值中自动获取。
var i, j int = 1, 3 // 自动获取;可以类型不同! var golang, java = true, "java~"
-
短变量声明:在函数中,可以用 “:=” 来代替 var 声明语句。(函数外不行,因为函数外要求语句以关键字开始)
func main(){ k := 3 i, golang, java := 3, true, "java~" }
-
基本类型: 居然没有字符= =
bool string // int, uint 和 uintptr 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。 // 整形应使用 int 类型,除非特需使用固定大小 or 无符号的整数类型。 int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr // uint8 的别名 byte // int32 的别名, 表示一个 Unicode 码点 rune // 两种大小的 float 型 float32 float64 // 两种大小的 复数 complex64 complex128
// var 也可以像 import 一样分组~ var ( myBool bool = true myString string = "fwy" )
-
默认值
// 数值默认 0 var i int var j float32 // 布尔默认 false var is bool // 字符串默认 ""(空串) var name string
-
类型转换:只能显式转换
// 注意:go 不支持隐式转换,设计者认为这个功能的弊端比好处多。 var i, j = 1, 2 var k float32 = float32(i + j) // 短声明 i, j := 1, 2 k := float32(i + j)
-
类型推导:变量类型由右值推导
var i int = 3 // 推导 j is type of int var j = i // 当右边包含未指明类型的数值常量时,新变量的类型取决于常量的精度: i := 3 // int j := 3.14 // float64 k := 3 + 3.14 // float64
-
常量: const 关键字。不能用 := 声明
// 常量可以是字符串、布尔值或数值 const world = "世界" // 可以使用分组、可以指定类型 const ( name int = 20 school = "SZU" )
-
数值常量:数值常量是高精度的 值。一个未指定类型的常量由上下文来决定其类型。
const ( Big = 1 << 100 Small = Big >> 99 ) func needInt(x int) int { return x } func needFloat(x float32) float32 { return x } func main(){ // 两个都可以跑,因为常量类型未指定 fmt.Println(needInt(Small)) fmt.Println(needFloat(Small)) fmt.Println(needFloat(Big)) fmt.Println(needInt(Big)) // error: overflows int }
二. 流程控制语句 for、switch、if 和 defer
-
循环语句:
sum := 0 /** * 1. go 只有一种循环:for 循环 * 2. 三个构成部分外没有小括号,用 ';' 隔开 * 3. 大括号{ }是必须的 */ for i := 0; i < 10; i++ { sum += i } // 4. 初始化 & 后置语句可省略 for ; sum < 100; { sum += sum } // 5. for 是 go 中的 "while"。去掉分号,有: for sum < 1000 { sum += sum } // 6. 条件也可省略。下例为“紧凑的无限循环” for { }
-
判断语句:
// 1. 和 for 一样:省略小括号,必须大括号 you := true if you { fmt.Println("you~you~you") } // 2. 和 for 一样:在条件表达式之前,可以执行一个简单语句 if me := 3 * 4; me == 12 { fmt.Prinln("简单语句声明的变量,作用域仅在该if语句中") } // 当然,对应的 else 块中也可以使用该变量 else { me += me }
-
switch 语句:
/** * 1. Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 * 2. 除非以 fallthrough 语句结束,否则分支会自动终止 (接1) * 3. case 无需为常量,且取值不必为整数 * 4. 同 if ,可以在先执行一个简单语句 */ func main(){ switch os := "macOS"; os { case "Linux": fmt.Println("LinuxOS") case "mac": fmt.Println("macOS") default: fmt.Println(os) } // 5. switch == switch true (无条件 switch 相当于 switch true) }
-
defer 语句:将函数推迟到外层函数返回之后执行。
参数会立即求值,但是函数要返回之后才调用。
func main(){ defer fmt.Println("hello") fmt.Print("world") // 输出:world hello }
-
defer 栈:推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
func main(){ for i := 1; i < 4; i++ { defer fmt.Print(i) } fmt.Print("end") // 输出:end 3 2 1 }
三. 更多类型 struct、slice 和映射
-
指针:
// 和 C++ 相同的两个运算符:*、& var( a int = 3 // 注意定义时 * 在类型前 p *int = &a ) fmt.Println(*p) // 零值为 nil p = nil
-
结构体:一个结构体(struct)就是一组字段(field)
// 声明格式 type Vertex struct { x int y int } func main(){ // 1. 大括号构造 temp := Vertex{1, 2} // 2. 用'.'来访问成员 fmt.Println(temp.x) // 3. 结构体指针 p := &temp // 4. 允许隐式间接引用 fmt.Println((*p).x) // 这样写太啰嗦了 fmt.Println(p.x) // 5. 使用 Name: 语法可以仅列出部分字段。(顺序无关) temp := Vertex{x: 1} // 1, 0 temp := Vertex{} // 0, 0 }
-
数组:
// var 格式 var a [2]int a[0] = 3 // := 格式 stinrgs := [2]string{"fwy", "szu"}
-
切片1:动态、灵活,在实践中比数组更常用
arr1 := [3]int{1, 2, 3} // 1. [low, height) 闭开区间 // 不存储数据,只是描述数组的一部分(共享同一个数组) sec1 := arr1[1 : 2] // 2. 创建一个数组,然后构建一个引用了它的切片(此处[]内没有大小声明) sec2 := []int{1, 2, 3} // 3. 以默认忽略上下界(下界为0,上界为切片长度) arr2 := [3]int{1, 2, 3} // 这四个切片实际上相同 sec3 := arr[: 3] sec4 := arr[0 :] sec5 := arr[:] sec6 := arr[0 : 3]
-
切片2:
// capacity:从切片第一个元素,到其底层数组元素末尾的个数 // length:包含的元素个数 s := []int{1, 2, 3, 4, 5} // 5, 5 fmt.Println(cap(s), len(s)) // 直接通过cap()、len() 获取 // 零值 nil:len 和 cap 都是0,而且【没有底层数组】 var ss []int
-
切片3:
make
函数:分配一个元素为零值的数组,并返回一个引用了它的切片 这也是分配动态数组的方式
a := make([]int, 5) // 指定容量,则加入第三个参数 b := make([]int, 5, 7) // 切片可包含任何类型,包括其它的切片。 board := [][]string{ []string{"-", "-", "-"}, []string{"-", "-", "-"}, }
-
切片4:append 追加元素
// 1. 追加到末尾 // 2. 容量不够时,会创造新的切片,然后再返回新切片 var s []int append(s, 0) // 3. 可一次添加多个元素 append(s, 1, 2, 3)
-
for 循环的 range 形式
// 1. 遍历切片时,每次会返回两个值:index、element的副本 var arr = []int{1, 2, 3} for i, v := range pow { fmt.Prinln(i, v) } // 2. 可以用 _ 来忽略返回值 for i, _ := range pow for _, v := range pow // 3. 如果只需要索引,也可以直接忽略第二个参数 for i := range pow
-
映射1:
// 1. 零值为 nil:既没有键,也不能添加键 // 2. make 函数会返回给定类型的映射,并将其初始化备用 m := make(map[string]int) m["fwy's age"] = 20 // 3. 必须有键名 var mm = map[string]int { "fwy" : 20 "JOJO" : 200 } // 4. 若顶级类型只是一个类型名,你可以在文法的元素中省略它。 var mmm = map[string]Vertex{ "Bell Labs": Vertex{ 40.68433, -74.39967, }, // 省略 "Google": { 37.42202, -122.08408, }, }
-
映射2:
// 增 or 改 m["fwy"] = 21 // 查 age = m["fwy"] // 删 delete(m, key) // contains:通过双赋值实现 element, ok = m[key] // 存在则 ok == true,否则 ok == false // 不存在则 element 为类型 nil 值 // 如果 element, ok 未声明,可以用短变量声明 element, ok := m[key]
-
函数
// 函数也是值。它们可以像其它值一样传递。(有点离谱。。但是也是这么个理) func compute(fn func(float64, float64) float64) float64{ return fn(3, 4) } // 翻译:compute函数的参数为“一个返回值为float型的,需要两个float参数的函数”,返回值为float类型 func main(){ // 这个也挺离谱的,整得有点像 Java 的匿名类了= = mySqrt := func(x, y float64) { return math.Sqrt(x * x, y * y) } compute(mySqrt()) }
-
函数的闭包
// 闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值 // adder() 的返回值类型 func(int) int func adder() func(int) int { sum := 0 return func(x int) int { sum += x return sum } } func main() { pos, neg := adder(), adder() fmt.Println(pos(1)) // 1 fmt.Println(neg(-1)) // -1 }
// 斐波那契闭包 func fibonacci() func() int { // 初始化,这行代码只跑了一次 a, b := 0, 1 // func() 一直引用了函数体外的a、b,并且进行了修改 return func() int { temp := a a, b = b, a + b return temp } func main() { fibo := fibonacci() for i := 0; i < 10; i++ { fmt.Println(fibo()) } } }
四. 方法和接口
-
方法:
// 1. go 没有类,但是可以给结构体定义“方法” // 2. 方法: 带特殊的“接收者参数”的函数(方法即函数) type Vertex struct { x int y int } // 3. 接收者参数,位于函数名前 func (v Vertex) getX() int { return v.x } // 4. 只能为包内定义的类型的接收者声明方法(包外,像int 之类的内建类型也不行) // 可以通过这种方法曲线救国 type myInt int func (v myInt) getInt() int { return (int)v } // 5. 为指针接收者声明方法 func (v *Vertex) scale(num int) { v.x *= num v.y *= num } // 如果不是指针,而是值,那么将会在副本上修改,而不是本体(对于函数的其他参数也是如此) func (v Vertex) scale(num int) { v.x *= num v.y *= num } // 6. 带指针参数的函数必须接受一个指针,而以指针为接收者的函数则“值和指针都可以” // 相反,值也是一样 p.Abs() // 会被解释为 (*p).Abs() // 7. 使用指针为接收者好处: // 1)可以修改值 // 2)不用复制值,造成资源浪费
-
接口 def
// 接口类型 是由一组方法签名定义的集合。 // 接口类型的变量可以保存任何实现了这些方法的值。 type Abser interface { Abs() float64 } type MyFloat float64 func (mf MyFloat) Abs() float64 { return float64(-mf); } type Vertex struct { x, y float64 } func (v *Vertex) Abs() float64 { return x + y } func main() { var ( abser Abser mf = math.Sqrt(30) v = Vertex{3, 4} ) abser = MyFloat(mf) abser.Abs() abser = &v abser.Abs() // error: abser = v }
// 1. 没有 implements 关键字,无需显示声明 // 2. 接口也是值,可以传递。 // 可以看做包含值和具体类型的元组:(value, type),会保存一个具体底层类型的具体值 // 3. 即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。“底层值为 nil 的接口值” // ps: 保存了 nil 具体值的接口其自身并不为 nil。 // 4. nil 接口值:和 3 不同,会产生运行时错误。“nil 接口值” // 也就是找不到具体方法的值,而 3 则是找到具体方法,找不到具体对象 type MyInterface interface { M() } func main() { var i MyInterface i.M() }
-
空接口:定义了 0 个方法的接口,可保存任何类型的值
// 被用来处理未知类型的值 func main() { var i interface{} fmt.Printf("(%v, %T)\n", i, i)// (<nil>, <nil>) i = 42 fmt.Printf("(%v, %T)\n", i, i)// (42, int) i = "hello" fmt.Printf("(%v, %T)\n", i, i)// (hello, string) }
**类型断言:**提供了访问接口值底层具体值的方式。
// t = i.(T) func main() { var i interface{} = "hello" s := i.(string) // hello s, ok := i.(string) // hello yes // 将会赋予对应零值 f, ok := i.(float64) // 0, false // 失败,且没有接收判断值的情况会报错 f := i.(float64) }
-
类型选择
func do(i interface{}) { switch v := i.(type) { case int : fmt.Println("int") case string: fmt.Println("string") default: fmt.Println("I don't know how %T !", v) } } func main() { do(32) do("hello") do(true) }
-
Stringer:属于 fmt 包
type Stringer interface { // 用于描述自己,类似 Java 的 toString() String() string }
-
错误:类似5,是一个内建接口
type error interface { Error() string } // 通常函数会返回一个 error 值, error 值为 nil 时成功,否则失败 type MyError struct { when time.Time what string } func (myError *MyError) Error() string { return fmt.Sprintf("%v, %s", myError.when, myError.what) } func run() error { return &MyError{ time.Now(), "it didn't work" } } func main() { if err := run(); err != nil { fmt.Println(err) } }
-
Reader: io 包
func main() { // 新建一个 Reader r := strings.NewReader("Hello Golang~"); // 新建一个容量为8的切片 mySlice := make([]byte, 8); for { // func (T) Read(b []byte) (n int, err error) n, err := r.Read(mySlice) fmt.Printf("n = %v err = %s mySlice = %v \n", n, err, mySlice) // 遇到数据流结尾时,会返回一个 EOF 错误 if err == io.EOF { break; } } }
-
待补充:图像
五. 并发
-
Go 程(goroutine): Go 运行时管理的轻量级线程。
// f, x, y 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。 // go f(x, y, z) func sum(s []int, c chan int) { sum := 0; for _, v := range s { sum += v } // 把 sum 的值送入流信道中 c <- sum } func main() { arr := []int{1, 2, 3, -1, -2 ,7} // 创建管道 c := make(chan int) // 各负责一半 go sum(arr[: len(arr)/2], c) go sum(arr[len(arr)/2 :], c) // 都结束后,再从信道中获取 x, y := <- c, <- c fmt.Println(x + y) }
-
带缓冲的信道
// 1. 通过在 make() 第二个参数设置缓冲 ch := make(chan int, 2) ch <- 1 ch <- 2 ch <- 3 // 填满后会阻塞
-
range & close
// 1. 可以通过分配第二个参数来判断信道是否关闭 ch = make(chan int) // 如果已经关闭了, ok = false v, ok := <-c // 2. 如果使用 range,会不断从信道接收值,直到他被关闭 for i := range c { }
-
select 语句
// 1. 使一个 Go 程可以等待多个通信操作。 // 2. 会阻塞到任一分支可行,如多个可行则随机一个 func fibonacci(c, quit chan int) { x, y := 0, 1 for { select { case c <- x: x, y = y, x + y // quit 可流出时 case <- quit: fmt.Println("quit") return default: fmt.Println("其他分支都没有准备好~") } } } func main() { c := make(chan int) quit := make(chan int) // 有 Java 匿名类内味了 go func() { for i := 0; i < 10; i++ { fmt.Println(<- c) } quit <- 1 }() fibonacci(c, quit) }