前言

开始学习Go语言, 轻量级+高效率+安全性, 综合性价比最高的语言, 所以打算之后以go作为工作语言, python只用来写脚本, c/c++只用来写核心算法
本篇将基础语言特性和常用语法总结起来, 作为学习记录

从函数相关概念开始记录, 其他基本概念看一遍应该不难

函数

Go的函数和方法是按值传递参数的, 所以所有的参数传递过去, 函数使用的均为原变量的副本

func关键字声明函数, 比如rand包中的Intn函数的声明

func Intn(n int) int

Intn为函数名
n为参数名
第一个int为参数类型
后一个int为函数返回值类型

一个包中的函数名, 如果首字母为大写, 可以被其他包调用, 如果是小写, 则只能在本包中调用, 类似python中的私有化方法, go语言中是用函数名首字母是否大写来进行区分

多参数声明情形:
如果多个参数均为同一类型, 则类型声明只需要一次, 放在最后

func Unix(sec, nsec int64) Time

如果不同, 则需要依次声明参数类型

func Example(a int, b float32, c float64) float64

多个返回值

// go中的Atoi函数调用
countdown, err := strconv.Atoi("10")// Atoi函数声明
func Atoi(s string) (i int, err error)

声明多个返回值时, 需要用括号将返回值括起来, 另外可以省略返回值名称只保留类型声明

可变参数函数

// fmt中Println的调用
fmt.Println(188)
fmt.Println(188, "height")// Println的声明
func Println(a ...interface{}) (n int, err error)

...表示函数参数数量可变
interface{}表示空接口, 即a的类型为空接口, 可以接收任意类型参数
...结合interface{}表示函数可以接收任意数量和任意类型的参数

方法

方法是与类型关联的函数, 可以理解为特殊的函数
type可以用来声明新类型

type celsius float64
var temperature celsius = 20

声明新类型可以提高代码可读性, 另外声明的新类型与原类型在go特性上依然是不同的类型, 如上例中celsiusfloat64不能混用, 否则报错

java与C#等语言中方法属于类, 而在go中没有提供类和对象, 方法即方法, 灵活性更大

type celsius float64
type kelvin float64func kelvinTocelsius(k kelvin) celsius {return celsius(k - 273.15)
}func (k kelvin) celsius() celsius {return celsius(k - 273.15)
}func main(){var k kelvin = 233var c celsius = k.celsius()fmt.Println(k, c)
}

第一个函数声明是传统函数, 第二个是关联kelvin类型的方法声明, 方法的参数可以没有也可以有多个, 前面的k kelvin作为接受者有且仅有一个, 并且接收者可以作为参数进行使用
(k kelvin)是接受者及方法类型声明
celsius()是函数名
celsius是返回值类型声明

往下看main函数是调用方法的例子, 方法是关联到变量类型的, 所以k是kelvin类型, 就可以通过.celsius()调用这个变量类型的方法, 转换为celsius类型返回给c
(可以类比为python中的实例方法, 实例化之后的对象, 可以调用类中声明的实例方法, 不过虽然都称为方法, go和python的方法的内涵还是有比较大的差别

简单实现kelvinTocelsius的方法, celsiusTofahrenheit的方法, kelvinTofahrenheit的方法

type celsius float64
type kelvin float64
type fahrenheit float64func (k kelvin) celsius() celsius {return celsius(k - 273.15)
}func (k kelvin) fahrenheit() fahrenheit {return k.celsius().fahrenheit()
}func (c celsius) fahrenheit() fahrenheit {return fahrenheit(c*9.0/5.0 + 32.0)
}func main() {var k kelvin = 300var c celsius = k.celsius()var f1 fahrenheit = k.fahrenheit()var f2 fahrenheit = c.fahrenheit()fmt.Println(k, c)fmt.Println(k, f1)fmt.Println(c, f2)
}

一等函数

即与变量均有同等地位, 函数可以赋值给变量, 也可以作为函数参数传递, 还可以作为返回值类型

type kelvin float64
func fakeSensor() kelvin {return kelvin(rand.Intn(201) + 150)
}func realSensor() kelvin {return 0
}func main() {sensor := fakeSensorfmt.Println(sensor())sensor = realsensorfmt.Println(sensor())
}

可以声明为

var sensor func() kelvin

sensor 变量名
func() 变量类型
kelvin 函数返回值类型声明

也可以type声明函数类型

type sensor func() kelvin

匿名函数

同其他语言的匿名函数一样, 不带名称的函数, 需要赋值给变量来引用, 如果是一次性的函数, 可以声明之后立即执行, 不用再给变量赋值引用

// 赋值给变量
var f = func() {fmt.Println("anonymous function")
}// 立即执行
func() {fmt.Println("anonymous function")
}()

闭包

匿名函数封闭并包围作用域中的变量, go语言中作用域就是{}花括号中变量声明的区域, 但是匿名函数可以捕获外围作用域的变量进行使用

func main() {var k kelvin = 294.0sensor := func() kelvin {return k}fmt.Println(sensor())k++fmt.Println(sensor())
}

sensor是引用了匿名函数, k虽然在外围作用域, 但是在匿名函数里使用到了k, 这就是匿名函数的闭包, 包围了外围作用域的变量

数组

var planets [8]stringplanets[0] = "Mercury"
planets[1] = "Venus"
planets[2] = "Earth"earth := planets[2]
fmt.Println(earth)
fmt.Println(planets[3] == "")

数组越界

Go编译器会自动检测数组越界问题, 如果检测出越界则报错
假设没发现越界错误, 运行时程序会panic, 崩溃

func main() {var planets [8]stringi := 8planets[i] = "pluto"pluto := planets[i]fmt.Println(planets[i])
}

上例在早前版本能够通过编译, 不过在运行时崩溃, 目前1.16版本已经可以在编译时检测出数组越界, 编译器更加智能了

复合字面值初始化数组

numbers1 = [5]string{"1", "2", "3", "4", "5"}numbers2 = [...]string{"1", "2", "3", "4", "5", "6", "7"}

...可以让go编译器自动计数数组元素个数

数组遍历

// 计数器方式遍历
for i := 0; i < len(numbers1); i++ {num := numbers1[i]fmt.Println(num)
} // range方式遍历, range第一个返回值时索引的下标, 第二个是元素的值
for i, num := range numbers1 {fmt.Println(i, num)
}

数组拷贝

将数组赋值给变量, 会产生完整的一个副本, 类似python中的深拷贝, 不仅仅是传递一个引用
数组作为函数参数时也是产生完整复制, 所以用长数组作为函数参数效率比较低
通常需要数组做参数一般采用slice切片类型, 而不是直接传数组, 切片是可变长的, 更灵活

数组的类型特性, 数组长度不同, go中视为不同类型, 即[5]string[8]string是两种不同的类型

二维数组

嗯, 就这样

var board [8][8]stringfor col := range board[2] {fmt.Println(col, board[2][col])
}

slice 切片

即指向数组的窗口, 指向数组一部分元素

s := "123456789"
sli := s[3:]
fmt.Println(sli)
//456789s = "987654321"
fmt.Println(sli)
//456789

观察上述例子, 切片后的值不受原数组的值得改变的影响
其他的操作与python基本一致, 包括左闭右开, 下标省略等, 不再赘述, 但是要注意一点不同, 虽然python允许负数切片, 但go中不存在负数slice

slice声明

numbers := [...]string{"1", "2", "3", "4", "5"}// 通过数组切片声明slice
numberslice1 := numbers[:]
// 直接声明slice, 数组前[]中留空即声明slice
numberslice2 := []string{"1", "2", "3", "4", "5"}

作为参数传递, slice类型不包括长度, 所以参数可以传递任意长度的slice

func hyperspace(worlds []string) {for i := range worlds {worlds[i] = strings.TrimSpace(worlds[i])}fmt.Println(worlds)
}func main() {planets := []string{"Venus   ", "Earth   ", "Mars   "}hyperspace(planets)fmt.Println(strings.Join(planets, ""))
}

可变数组

可变长数组, 在go中是slice, 可以使用append内置函数实现增加元素

长度(length)与容量(capacity)的关系
长度即元素个数, 容量是针对slice而言, slice的底层数组的长度会大于等于slice的长度, 即容量大于长度, 当容量不足增加元素时, go会自动扩展底层数组到更大的容量, 该过程对程序员透明, 不需要程序员考虑, 容量扩大后, slice可以append更多元素, 长度相应增加

func dump(lable string, slice []string) {fmt.Printf("%v: length %v, capacity: %v %v\n", lable, len(slice), cap(slice), slice)
}func main() {numbers1 := []string{"1", "2", "3", "4", "5"}numbers2 := append(numbers1, "6")numbers3 := append(numbers2, "7", "8")dump("numbers1", numbers1)dump("numbers2", numbers2)dump("numbers3", numbers3)
}// numbers1: length 5, capacity: 5 [1 2 3 4 5]
// numbers2: length 6, capacity: 10 [1 2 3 4 5 6]
// numbers3: length 8, capacity: 10 [1 2 3 4 5 6 7 8]// 循环append元素, 查看capacity的变化情况
func main() {s := []int{1, 2, 3}length, capacity := len(s), cap(s)for i := 0; i < 1000; i++ {s = append(s, i)if capacity != cap(s) {length = len(s)capacity = cap(s)fmt.Println(length, capacity)}}
}// 4 6
// 7 12
// 13 24
// 25 48
// 49 96
// 97 192
// 193 384
// 385 768
// 769 1536

切片第三索引
与python不同, 索引有三个, 不过第三个是表示切片的容量限制

切片预分配
类似c的动态内存管理, 为了申请1KB的动态内存, 实际系统会拿到MB大小的内存以备不时之需, 所以slice通过make预分配也采用这种策略, 提高分配效率, 而不是等需要时再去申请

// 只有两个参数时, 第二个参数既表示长度, 也表示容量
numbers := make([]string, 10)
// 有三个参数时, 第二个参数表示长度, 第三个参数表示容量
numbers := make([]string, 0, 10)

slice传参语法糖

func terraform(prefix string, worlds ...string) []string {newWorlds := make([]string, len(worlds))for i := range worlds {newWorlds[i] = prefix + " " + worlds[i]}return newWorlds
}func main() {planets := []string{"Venus", "Mars", "Earth"}newPlanets := terraform("New", planets...)fmt.Println(newPlanets)
}

terraform第二个参数是任意数量的string类型, 若想要传slice进去则需要在参数后加...表示展开slice的每一个元素单独传递参数, slice每一个元素的类型是string, 所以满足函数声明的参数类型

map

声明map

temperature := map[string]int{"Earth": 15,"Mars": -65,
}

map关键词
[string]key类型
intvalue类型

map需要注意引用特性, 如果一个变量A表示map类型, 当赋值给另一个变量B时, 变量B所引用的是A本身, 修改B也会导致A变化, 对比数组类型, 数组的赋值是完整拷贝, map则是引用

内置删除函数delete, 删除map中的特定key

func main() {planets := map[string]string{"Earth": "Sector ZZ9","Mars":  "Sector ZZ9",}planetsMarkII := planetsplanets["Earth"] = "whoops"fmt.Println(planets)fmt.Println(planetsMarkII)delete(planets, "Earth")fmt.Println(planetsMarkII)fmt.Println(planets)
}

注意语言特性

map[string]string{} // 正确map[string]string {} // 错误, map类型与之后的{花括号之间不能有空格

map的make预分配
如果没有初始化map, 则在声明时必须使用make给map分配空间, 初始长度为0, make给map分配空间时的第一个参数是类型, 第二个是分配给map的key的数量

temperature := make(map[string]int, 8)fmt.Println(temperature)

map与slice综合应用

(1) 实现siice分类
每10°划分一个map的key

func main() {temperatures := []float64{-28.0, 32.0, -31.0, 29.0, -23.0, 25.0, -33.0, 28.0,}groups := make(map[float64][]float64)for _, t := range temperatures {g := math.Trunc(t/10) * 10groups[g] = append(groups[g], t)}for g, temperatures := range groups {fmt.Printf("%v: %v\n", g, temperatures)}
}// -20 [-28 -23]
// 30 [32]
// -30 [-31 -33]
// 20 [29 25 28]

解释一下稍微复杂的语法
make(map[float64][]float64)
map的key为float64类型, value是[]float64slice类型
math.Trunc是将实数的小数部分去掉

(2) map用作set
检测温度是否出现, 出现则在set中设置温度的value = true, 然后按升序输出set, 这里因为map不会自动排序key/value, 所以需要使用sort按map的value升序排序, 再输出即满足要求

func main() {temperatures := []float64{-28.0, 32.0, -31.0, 29.0, -23.0, 25.0, -33.0, 28.0,}set := make(map[float64]bool)for _, t := range temperatures {set[t] = true}fmt.Println(set)unique := make([]float64, 0, len(set))for t := range set {unique = append(unique, t)}sort.Float64s(unique)fmt.Println(unique)
}// map[-33:true -31:true -28:true -23:true 25:true 28:true 29:true 32:true]
// [-33 -31 -28 -23 25 28 29 32]

结构 struct

同c/c++中的结构体, 将零散的组件构成一个完整的结构体, Go中提供struct类型
声明struct, 比如位置经纬度

var position struct{lat float64long float64
}//声明为复用类型
type position struct{lat float64long float64
}// 声明时初始化
example1 := position{lat: -123.456, long: 345.678}
// 也可以省略结构体属性, 但是必须按顺序赋值
example2 := position{-123.456, 345.678}

struct用json导出
这里有个大坑, go语言特性中有一条, 变量/函数名首字母大写才具有导出性质, 或者被其他包使用的性质, 所以json化一个struct类型变量时, 如果struct的属性名声明首字母没大写, 则无法导出

func main() {type position struct {Lat, Long float64 // 注意大写首字母}Spaceship := position{-4.444, 5.555}bytes, err := json.Marshal(Spaceship)exitOnError(err)fmt.Println(string(bytes))
}func exitOnError(err error) {if err != nil {fmt.Println(err)os.Exit(1)}
}// {"Lat":-4.444,"Long":5.555}

struct自定义json的字段名

type position struct {Lat float64 `json:"latitude"`Long float64 `json:"longitude"`
}

替换上面的结构声明, 执行程序可以看到json的字段名被更改

func main() {type position struct {Lat  float64 `json:"latitude"`Long float64 `json:"longitude"`}Spaceship := position{-4.444, 5.555}bytes, err := json.Marshal(Spaceship)exitOnError(err)fmt.Println(string(bytes))
}func exitOnError(err error) {if err != nil {fmt.Println(err)os.Exit(1)}
}// {"latitude":-4.444,"longitude":5.555}

同理可以更改xml的字段名

type position struct {Lat float64 `xml:"latitude"`Long float64 `xml:"longitude"`
}

class

下面来讲Go的class知识… 开玩笑!

Go里是没有class这个概念的, 同时也没有对象, 继承等概念, Go不是一个经典的面向对象语言, 不过Go依然可以通过本身的特性实现面向对象
即通过struct方法实现
上面描述过, 方法是关联到特定类型的函数, 如果把方法关联到struct, 即可实现java/c++中类的结构, 一个类既有属性, 也有方法, 不过在go中就是带有方法的struct, 与面向对象的设计理念没有本质区别

type coordinate struct {degrees, minutes, seconds float64hemisphere                rune
}func (c coordinate) decimal() float64 {sign := 1.0switch c.hemisphere {case 'W', 'S', 'w', 's':sign = -1.0}return sign * (c.degrees + c.minutes/60 + c.seconds/3600)
}type position struct {lat, long float64
}func newPostion(lat, long coordinate) position {return position{lat.decimal(), long.decimal()}
}func main() {lat := coordinate{4, 22.5, 32, 'W'}long := coordinate{123, 45, 31.2, 'E'}fmt.Println(lat.decimal(), long.decimal())spaceship := position{lat.decimal(), long.decimal()}fmt.Println(spaceship)
}// -4.3838888888888885 123.75866666666667
// {-4.3838888888888885 123.75866666666667}

Go中没有专门的构造函数概念, 但是规定new开头后接变量名的函数即用于构造该变量的函数
注意区分官方包的New函数, 如error.New(), 因为go通过包名和函数名进行调用, 所以New函数名并不冲突

组合与转发

与经典面向对象语言不同, Go通过结构体的组合实现组合, 另外通过嵌入(embedding)的特性实现方法的转发(forwarding)
结构体的简单组合

type report struct {sol         inttemperature temperatureposition    position
}type position struct {lat, long float64
}type temperature struct {high, low celsius
}type celsius float64func main() {bradbury := position{-4.555, 5.777}t := temperature{high: 20, low: -50}report := report{sol:         150,temperature: t,postion:     bradbury,}fmt.Println(report)fmt.Printf("a balmy %v°C\n", report.temperature.high)
}

方法的转发, 可以简单理解为结构体中的下层结构体的方法转发到上层结构体进行调用

func (t temperature) average() celsius {return (t.high + t.low) / 2
}func (r report) average() celsius {return r.temperature.average()
}func main() {bradbury := position{-4.555, 5.777}t := temperature{high: 20, low: -50}fmt.Println(t.average())report := report{sol:         150,temperature: t,postion:     bradbury,}fmt.Println(report)fmt.Println(report.temperature.average())fmt.Printf("a balmy %v°C\n", report.temperature.high)
}

其中

func (t temperature) average() celsius {return (t.high + t.low) / 2
}func (r report) average() celsius {return r.temperature.average()
}

即为方法的转发, 从report调用temperatureaverage方法, 看起来比较鸡肋

第二种转发实现, struct嵌入

type report struct {sol inttemperatureposition
}

省略变量名, 只有变量类型, 就可以让go构建结构体方法的调用链, 不需要声明方法转发, report也能调用temperatureaverage方法

func (t temperature) average() celsius {return (t.high + t.low) / 2
}// func (r report) average() celsius {
//  return r.temperature.average()
// }func main() {bradbury := position{-4.555, 5.777}t := temperature{high: 20, low: -50}fmt.Println(t.average())report := report{sol:         150,temperature: t,position:    bradbury,}fmt.Println(report)fmt.Println(report.temperature.average())fmt.Printf("a balmy %v°C\n", report.temperature.high)
}

注: struct可以转发任意类型, 包括内置类型, 比如 int, float64, string…

接口

Go中接口不需要显式声明, 通过列举类型必须满足的一组方法进行声明

var t interface {talk() string
}type martian struct {
}func (m martian) talk() string {return "knock knock"
}type laser intfunc (l laser) talk() string {return strings.Repeat("rush rush\n", int(l))
}func main() {t = martian{}fmt.Println(t.talk())t = laser(3)fmt.Print(t.talk())
}

这里声明接口t需要满足存在talk() string方法, 一个类型满足该要求, 则可赋值一个接口

var t interface {talk() string
}

这里有个套娃特性, 就是struct的嵌入特性与接口可以混合使用

type talker interface {talk() string
}type laser intfunc (l laser) talk() string {return strings.Repeat("rush rush\n", int(l))
}func shout(t talker) {louder := strings.ToUpper(t.talk())fmt.Println(louder)
}type startship struct {laser
}func main() {s := startship{laser: 3}fmt.Print(s.talk())shout(s)
}// rush rush
// rush rush
// rush rush
// RUSH RUSH
// RUSH RUSH
// RUSH RUSH

startship类型中嵌入了laser类型, laser中有talk()方法, 所以startship也有talk()方法, 因此满足talker接口要求, 则s是一个接口, shout(s)则执行talk()方法, 所以会输出大写的RUSH * 3

注: go的接口规定以er命名结尾

指针

Go的指针, 原理与c/c++类似(毕竟go的基础之一是c), 但是go中不存在野指针(dangling pointer), go的指针也经过简化与优化, 满足安全性要求

&

go中通过&获取变量地址, 但是注意, &不能获得字面值(常量)地址

s := "test addr"
fmt.Println(&s)
// fmt.Println(&"test addr")

*

go中*获取变量地址的值

在Go中不允许地址的自增操作, address++
使用举例

func main() {canada := "Canada"var home *string = &canadafmt.Println(home)fmt.Println(*home)*home = "adanaC"fmt.Println(canada)var second *string = homefmt.Println(*second, *home)*second = "changed place"fmt.Println(*second, *home)var third string = *homefmt.Println(third, *second)third = "third place"fmt.Println(third, *second)
}// 0xc000046240
// Canada
// adanaC
// adanaC adanaC
// changed place changed place
// changed place changed place
// third place changed place

注意这里的Go语言特性, var third string = *home用指针指向的地址的值给一个变量赋值时, 会复制完整副本, 所以third的地址与secondhome不等

结构体指针

go的struct指针比较特殊, 允许&取复合字面值(即常量)struct的地址, 并且通过指针引用struct的成员有两种方式

func main() {type person struct {name, superpower stringage              int}timmy := &person{name: "Tommy",age:  20,}// 第一种方式, 用(*timmy)取struct类型(*timmy).superpower = "laser eye"fmt.Println(*timmy)// 第二种方式, 直接取timmy.superpower = "frost hand"fmt.Println(*timmy)
}

数组指针

struct, 可以&复合字面值取地址, 数组索引和切片时会自动解引用(取*), 所以直接在数组之后用[]就行, 这个特性类似C, 不过又不同于C, Go的指针和数组变量是相互独立的类型, 数组变量就是数组类型, 指针就是指针类型
此外slice和map类型也可以直接&复合字面值, 但是没有自动解引用特性, 所以需要手动*再取元素

参数指针 / 接收者指针

type person struct {name stringage  int
}func (p *person) brithday() {p.age++
}func main() {terry := &person{name: "John",age:  23,}terry.brithday()fmt.Println(terry.age)
}// 24

采用接收者指针, 所以方法birthday修改的是原变量的成员值, 对比如果接收者采用struct变量

type person struct {name stringage  int
}func (p person) brithday() {p.age++
}func main() {terry := person{name: "John",age:  23,}terry.brithday()fmt.Println(terry.age)
}//23

所以指针的用处, 就是让函数可以修改原变量, 否则就是得用原变量的副本, (上面函数那一节说了go语言的特性, 函数与方法的参数传递都是按值传递

内部指针

&可以获取结构体内部字段(成员)的地址

type stats struct {level             intendurance, health int
}func levelup(s *stats) {s.level++s.endurance = 47 + (14 * s.level)s.health = 5 * s.endurance
}type character struct {name stringstats
}func main() {player := character{name: "Kotlin"}levelup(&player.stats)fmt.Println(player.stats)
}

&player.stats就是获取player.stats的地址

隐式指针

map类型就是一个隐式使用指针的类型, map在作为函数/方法参数传递时不是按值传递, 这个类型比较特殊, 它会传递map的引用, 也就是指针到函数进行使用, 其实map的本质就是一种隐式指针

另外slice类型, 按上面slice那节的描述, 是指向数组的窗口, 不过slice在指向数组元素时也用到了隐式指针, slice内部结构包含3个部分

// 数组指针
// slice容量
// slice长度

slice传递至函数/方法时, slice内部指针可以直接修改底层数组
对比指向slice的指针

func reclassify(planets *[]string) {*planets = (*planets)[:4] //注意[]优先级大于*, 所以需要(*planets)先解引用
}func main() {planets := []string{"Mercury", "Venus", "Earth", "Mars","Jupiter", "Saturn", "Uranus", "Neptune","Pluto",}reclassify(&planets)fmt.Println(planets)
}// [Mercury Venus Earth Mars]

指向slice的指针, 只能修改slice的长度, 容量, 起始偏移, 即修改slice本身, 但是不能修改底层数组

指针与接口

如果方法声明接收者是非指针变量类型, 则接口参数可以使用非指针也可以使用指针类型, 两者等效

如果方法是声明的指针类型, 则需要注意差异, 只有指针参数正确

func shout(t talker) {louder := strings.ToUpper(t.talk())fmt.Print(louder)
}type talker interface {talk() string
}type laser intfunc (l *laser) talk() string {return strings.Repeat("come on\n", int(*l))
}func main() {t := laser(4)shout(&t) // 正确shout(t) // 错误
}

nil

nil表示零/无/空指针
go的nil需要谨慎处理, *nil会出现panic(程序崩溃)
所以如果使用指针, 需要在函数开始习惯性检查指针是否是nil, 就像开车之前先系安全带一样

func, slice, map, interface 没有赋值初始化, 则默认为nil

错误处理

多个返回值, 一般规定最后一个是err, 表示是否出错, 没有则err == nil

文件写入

可能包含

  1. 路径不正确
  2. 权限不足
  3. 磁盘空间不足

等原因

func proverbs(name string) error {f, err := os.Create(name)if err != nil {return err}_, err = fmt.Fprintln(f, "Errors are values")if err != nil {f.Close()return err}_, err = fmt.Fprintln(f, "Don`t just check errors, handle them gracefully")f.Close()return err
}func main() {err := proverbs("proverbs.txt")if err != nil {fmt.Println(err)os.Exit(1)}
}

error是一个内置类型, 表示错误

defer

确保语句在函数返回前执行

func proverbs(name string) error {f, err := os.Create(name)if err != nil {return err}defer f.Close()_, err = fmt.Fprintln(f, "Errors are values")if err != nil {f.Close()return err}_, err = fmt.Fprintln(f, "Don`t just check errors, handle them gracefully")f.Close()return err
}

如果一个函数有多个return, 只需要有一个defer即可, 能够确保函数返回前执行defer之后的语句, 比如上例中, defer f.Close()如果任意个错误出现/正常返回, go都会先关闭文件, 确保不会因为突然函数返回导致文件出错

自定义错误

error类型也是一个内置接口, 任何类型只要实现了返回string类型的Error()方法都是error接口

const rows, cols = 9, 9type Grid [rows][cols]int8var (ErrBounds = errors.New("Out of bounds")ErrDigits = errors.New("Invalid digit")
)type SudokuError []errorfunc (se SudokuError) Error() string {var s []stringfor _, err := range se {s = append(s, err.Error())}return strings.Join(s, ", ")
}func (g *Grid) Set(row, col int, digit int8) error {var errs SudokuErrorif !inBounds(row, col) {errs = append(errs, ErrBounds)}if !validate(digit) {errs = append(errs, ErrDigits)}if len(errs) > 0 {return errs}g[row][col] = digitreturn nil
}func inBounds(row, col int) bool {if (row < 0 || row >= rows) || (col < 0 || col >= cols) {return false}return true
}func validate(digit int8) bool {if digit < 0 || digit >= 127 {return false}return true
}func main() {var g Griderr := g.Set(4, 5, 15)if err != nil {switch err {case ErrDigits, ErrBounds:fmt.Println("digit or bounds error shows up!")default:fmt.Println(err)}os.Exit(1)}
}

这里自定义了error

var (ErrBounds = errors.New("Out of bounds")ErrDigits = errors.New("Invalid digit")
)

注意errors.New()生成的是指向错误的指针, 而不是错误字符串

而且非指针变量可以适用指针接收者方法, 修改的是变量本身的值

func (g *Grid) Set(row, col int, digit int8) errorvar g Grid
err := g.Set(4, 5, 15)

类型断言

通过error.(Err)形式断言类型, 如果为真返回类型本身与bool值

func main() {var g Griderr := g.Set(14, 5, 15)if err != nil {if errs, ok := err.(SudokuError); ok {fmt.Printf("%d errors(s) occurred: \n", len(errs))for _, err := range errs {fmt.Printf("- %s\n", err)}}os.Exit(1)}
}// 1 errors(s) occurred:
// - Out of bounds
// exit status 1

panic

直接用内置函数panic进行调用
panic("something is wrong")

recover

内置recover函数可从panic中转出, 不让程序崩溃

goroutine

go中的独立任务称为goroutine
类似于其他语言中的协程, 进程, 线程, 但又不完全一样
众所周知, go本身就是并发语言, 直接支持并发(concurrent)操作
对比其他编程语言, 顺序代码修改为并发代码需要大改, 不过go通过goroutine直接就能并发执行代码段, 这也是go的强大之处

启动goroutine

直接在调用函数之前加上关键词go即可

通道channel

为了知道goroutine是否执行结束, 使用channel
此外还可以用于多个goroutine之间安全传递信息
可以用作变量, 函数参数, 结构体字段
使用make创建通道

c := make(chan int)

从通道发送值 c <- 1
从通道接收值 r := <- c
执行发送的goroutine会一直等到另一个goroutine接收为止, 相当于阻塞式通信, 如果对面一直不接收, 该goroutine会一直处于等待状态, 反之亦然, 处于接收状态的goroutine会一直等待对面发送信息

看个简单例子

func main() {c := make(chan int)for i := 0; i < 5; i++ {go sleepinggo(i, c)}for i := 0; i < 5; i++ {gopherID := <-cfmt.Println("gopherID: ", gopherID, "wake ups")}
}func sleepinggo(id int, c chan int) {time.Sleep(time.Second)fmt.Println("sleeping ...", id, "...")c <- id
}

select处理多通道

time.After返回一个通道, 在特定时间之后接受值
select会在case通道就绪时执行通道的操作, 如果不包含任何case语句, 将处于持续等待状态, 不会结束
不用make初始化的通道chan默认为nil, 用nil通道进行接收发送, 会导致永久阻塞, 对nil通道调用close(), 会引发panic

func main() {c := make(chan int)for i := 0; i < 10; i++ {go sleepinggo(i, c)}timeout := time.After(2 * time.Second)for i := 0; i < 10; i++ {select {case gopherID := <-c:fmt.Println("gopher", gopherID, "wakes up")case <-timeout:fmt.Println("Out of time limit")return}}
}func sleepinggo(id int, c chan int) {time.Sleep(time.Duration(rand.Intn(4000)) * time.Millisecond)c <- id
}

select的case条件是执行了通道的接收/发送则进入case, timeout是由time.After赋值的通道, case <-timeout超过特定时间传入了通道, 则触发该case, 打印Out of time limit然后返回

死锁

多个goroutine就离不开死锁的概念, 如果一个goroutine一直在等待某个不可能发生的接收/发送事件, 则程序出现死锁, go中出现死锁则会崩溃或挂起

并发状态

两个概念:
共享值
竞争条件 (race condition)

多个goroutine共享一个值可能会导致冲突和非预期错误, 为了解决共享值的冲突问题引入互斥锁(mutex = mutual exclusive), (和操作系统的进程管理相似)
基于两个sync包的函数实现互斥锁, 注意互斥锁并不是go语言自带的特性
Lock()
Unlock()

事件循环 goroutine 替代 中心循环

go语言不同于其他多数语言, 对时间相关的情形进行模拟时需要一个中心循环表示事件发展, go则用goroutine作为核心机制消除了中心循环的需求, 每个goroutine都是一个独立的事件循环
举个模拟spaceship移动的事件例子, 只通过goroutine之间的通道发送/接收实现

type command intconst (right = command(0)left  = command(1)
)type RoverDriver struct {conmmandch chan command
}func NewRoverDriver() *RoverDriver {r := &RoverDriver{conmmandch: make(chan command),}go r.drive()return r
}func (r *RoverDriver) drive() {pos := image.Point{X: 0, Y: 0}direction := image.Point{X: 1, Y: 0}updateInterval := time.Millisecond * 250nextMove := time.After(updateInterval)for {select {case c := <-r.conmmandch:switch c {case right:direction = image.Point{X: -direction.Y, Y: direction.X}case left:direction = image.Point{X: direction.Y, Y: -direction.X}}log.Printf("new direction: %v", direction)case <-nextMove:pos = pos.Add(direction)log.Printf("move to %v", pos)nextMove = time.After(updateInterval)}}
}func (r *RoverDriver) Left() {r.conmmandch <- left
}func (r *RoverDriver) Right() {r.conmmandch <- right
}func main() {r := NewRoverDriver()time.Sleep(3 * time.Second)r.Left()time.Sleep(3 * time.Second)r.Right()time.Sleep(3 * time.Second)
}

驾驶员RoverDriver的构造函数NewRoverDriver()生成并开始并发执行go r.drive(), drive()函数是驾驶员RoverDrive类型的方法, 其事件循环是基于select语句和chan通道的接收操作, 每隔250毫秒朝direction方向移动一个单位得到更新的position, 没有依赖中心循环, 只依靠RoverDriver自身的goroutine完成事件循环

总结

初步将go的基本语法和语言特性学习一遍, 毕竟语言只是工具, 之后需要实操各个项目提高代码能力和积累开发经验

参考

微软MVP的Go语言快速入门教程
Go语言中文网
Go官方中文文档

Go学习笔记 一篇到底相关推荐

  1. MySQL学习笔记-基础篇1

    MySQL 学习笔记–基础篇1 目录 MySQL 学习笔记--基础篇1 1. 数据库概述与MySQL安装 1.1 数据库概述 1.1.1 为什么要使用数据库 1.2 数据库与数据库管理系统 1.2.1 ...

  2. C# 学习笔记入门篇(上)

    文章目录 C# 学习笔记入门篇 〇.写在前面 Hello World! 这篇学习笔记适合什么人 这篇学习笔记到底想记什么 附加说明 一.命名空间 "进入"命名空间 嵌套的命名空间. ...

  3. vue-resource post php,Vue学习笔记进阶篇——vue-resource安装及使用

    简介 vue-resource是Vue.js的一款插件,它可以通过XMLHttpRequest或JSONP发起请求并处理响应.也就是说,$.ajax能做的事情,vue-resource插件一样也能做到 ...

  4. [mmu/cache]-ARM MMU的学习笔记-一篇就够了

    ★★★ 个人博客导读首页-点击此处 ★★★ . 说明: 在默认情况下,本文讲述的都是ARMV8-aarch64架构,linux kernel 64位 . 相关文章 1.ARM cache的学习笔记-一 ...

  5. [mmu/cache]-ARM cache的学习笔记-一篇就够了

    ★★★ 个人博客导读首页-点击此处 ★★★ . 说明: 在默认情况下,本文讲述的都是ARMV8-aarch64架构,linux kernel 64位 . 相关文章 1.ARM MMU的学习笔记-一篇就 ...

  6. Vue学习笔记进阶篇——Render函数

    本文为转载,原文:Vue学习笔记进阶篇--Render函数 基础 Vue 推荐在绝大多数情况下使用 template 来创建你的 HTML.然而在一些场景中,你真的需要 JavaScript 的完全编 ...

  7. PHP学习笔记 - 进阶篇(7)

    PHP学习笔记 - 进阶篇(7) 文件操作 读取文件内容 PHP具有丰富的文件操作函数,最简单的读取文件的函数为file_get_contents,可以将整个文件全部读取到一个字符串中. $conte ...

  8. Vue学习笔记入门篇——数据及DOM

    本文为转载,原文:Vue学习笔记入门篇--数据及DOM 数据 data 类型 Object | Function 详细 Vue 实例的数据对象.Vue 将会递归将 data 的属性转换为 getter ...

  9. WPF学习笔记(数据绑定篇3)

    接上回的<WPF学习笔记(数据绑定篇2)>,继续 BindValidation 此示例演示了: 如何使用错误模板: 使用样式显示错误信息: 如何在校验发生异常时执行回调: 首先,你可以看见 ...

  10. Vue学习笔记进阶篇——多元素及多组件过渡

    本文为转载,原文:Vue学习笔记进阶篇--多元素及多组件过渡 多元素的过渡 对于原生标签可以使用 v-if/v-else.但是有一点需要注意: 当有相同标签名的元素切换时,需要通过 key 特性设置唯 ...

最新文章

  1. getaddrinfo()函数详解
  2. byte[]数组下标的最大值
  3. Windows 10将为大型企业提供订阅型服务
  4. mysql之DDL操作--数据库
  5. 淘宝技术沙龙「系统稳定性与性能」的笔记与思考
  6. LeetCode 2042. 检查句子中的数字是否递增
  7. oracle 取mac地址,java执行命令,得到Mac地址
  8. python 聚类_聚类算法中的四种距离及其python实现
  9. Web核心技术-服务器端技术
  10. 无法在web服务器上启动调试。调试失败,因为没有启用集成windows身份验证
  11. java多线程写数据到数据库6_java多线程向数据库写入数据
  12. 传说中的考研神校,考研人数究竟有多高?
  13. php调用API支付接口 可个人使用,无需营业执照(使用第三方接口,调用的天工接口。)...
  14. 半年总结——思想的转变
  15. 数量遗传学 第二章 群体的遗传组成
  16. 波动方程的行波解(一)| 一维波动方程的通解和初值问题的达朗贝尔(d' Alembert)公式 | 偏微分方程(九)
  17. PEO-b-PTMPM的嵌段共聚物复合囊泡/具有pH响应性的纳米颗粒/卤化银纳米粒子/聚合物纳米
  18. C#/Winform 节点拖放-TreeView控件
  19. OpenBLAS学习一:源码架构解析GEMM分析
  20. 网银数字证书很“尴尬”

热门文章

  1. 带log的计算器html代码,lg计算器(log计算器在线)
  2. java提取富文本文字_富文本中文字部分提取
  3. Chromium浏览器不能播放MP4
  4. 网络安全-典型的恶意代码
  5. 计算机算单元格个数,罕见知识点–Excel 参数这样用,才能算出区域内文本单元格的数量...
  6. 日志追踪-Java字节码-类文件结构
  7. CSS3(新增样式)
  8. CprimePlus 函数2
  9. Python爬虫(三):python抓取网页中的图片到本地
  10. 关于MSTP的个人总结,如何查看华为生成树状态信息