GO语言学习笔记

第一章 Go语言简介

  1. 协程是go最显著的特性
  2. 最好读一读go语言源码 ,项目链接:https://github.com/golang/go
  3. go语言只支持i++,别的写法会报错
  4. GOPATH 是一个路径,用来存放开发中需要用到的代码包。
  5. linux中需要设置 GOROOT(安装目录) 和 PATH,把安装目录追加到PATH中,export PATH=$ PATH:$ GOROOT/bin:$GOBIN
  6. 如果想要构建一个项目,就需要将这个项目的目录添加到 GOPATH 中,多个项目之间可以使用;分隔
  7. go语言项目的目录一般包含三个子目录 : src, pkg, bin, 包名与目录名应该一一对应
  8. Go语言会把通过go get 命令获取到的库源文件下载到 src 目录下对应的文件夹当中
  9. godep是一个go语言官方通过vender模式来管理第三方依赖的工具
  10. Go语言以“包”作为管理单位,每个 Go 源文件必须先声明它所属的包
  11. import 语句,用于导入程序中所依赖的包,导入的包名使用双引号""包围
  12. go build命令可以将Go语言程序代码编译成二进制的可执行文件
  13. go run命令 会在编译后直接运行Go语言程序,编译过程中会产生一个临时文件,但不会生成可执行文件,很适合用来调试程序。

第二章 GO语言基本语法

  1. 声明变量的一般形式是使用 var 关键字:
var a, b *int
  1. 当一个变量被声明之后,系统自动赋予它该类型的零值,所有的内存在 Go 中都是经过初始化的。
  2. 变量的命名规则遵循骆驼命名法,即首个单词小写
  3. 批量申明变量
var (
    a int
    b string
    c []float32
    d func() bool
    e struct {
        x int
    }
)
  1. 可使用更加简短的变量定义和初始化语法
i, j := 0, 1

6.变量初始化的标准格式

//var 变量名 类型 = 表达式
var hp int = 100
  1. 在标准格式的基础上,将 int 省略后,编译器会尝试根据等号右边的表达式推导 hp 变量的类型。
var hp = 100
  1. var 的变量声明还有一种更为精简的写法
//短变量声明并初始化
//如果 hp 已经被声明过,但依然使用:=时编译器会报错
hp := 100
  1. 交换变量
var a int = 100
var b int = 200
b, a = a, b

10.匿名变量的特点是一个下画线“”,“”本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

    a, _ := 1, 2
    _, b := 3, 4

11.函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。
12. 在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源文件需要使用“import”关键字引入全局变量所在的源文件之后才能使用这个全局变量。
13. 全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。
14. 一个 float32 类型的浮点数可以提供大约 6 个十进制数的精度,而 float64 则可以提供约 15 个十进制数的精度,通常应该优先使用 float64 类型
15. 一个布尔类型的值只有两种:true 或 false
16. 可以使用双引号""来定义字符串,字符串中可以使用转义字符来实现换行、缩进等效果

var str = "GO语言从入门到放弃\nGO语言从入门到放弃"
//两个字符串 s1 和 s2 可以通过 s := s1 + s2 拼接在一起
str += "\n"
//嵌入一个多行字符串时,就必须使用`反引号
const s = `
第一行
第二行
第三行
`
  1. 在 ASCII 码表中,A 的值是 65,使用 16 进制表示则为 41,所以下面的写法是等效的
//(\x 总是紧跟着长度为 2 的 16 进制数)
var ch byte = 65var ch byte = '\x41'      

18.一个类型的值可以被转换成另一种类型的值

//类型 B 的值 = 类型 B(类型 A 的值)
//不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)
a := 5.0
b := int(a)

19.当一个指针被定义后没有分配到任何变量时,它的默认值为 nil。指针变量通常缩写为 ptr

//Go语言中使用在变量名前面添加&操作符(前缀)来获取变量的内存地址
ptr := &v    // v 的类型为 T
var cat int = 1
var str string = "banana"
fmt.Printf("%p %p", &cat, &str)

20.Go语言还提供了另外一种方法来创建指针变量,格式如下:

str := new(string)
*str = "Go语言教程"
fmt.Println(*str)

21.变量逃逸分析: 通过编译器分析代码的特征和代码的生命周期,决定应该使用堆还是栈来进行内存分配。
22. 变量的生命周期与变量的作用域有着不可分割的联系
23. Go语言中的常量使用关键字 const 定义,并且只能是布尔型、数字型(整数型、浮点型和复数)和字符串型

//常量的值必须是能够在编译时就能够确定的
const pi = 3.14159 // 相当于 math.Pi 的近似值

24.iota 常量生成器: 在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一。

type Weekday int

//周日将对应 0,周一为 1,以此类推。
const (
    Sunday Weekday = iota
    Monday
    Tuesday
    Wednesday
    Thursday
    Friday
    Saturday
)
//String() 方法的 ChipType 在使用上和普通的常量没有区别。
//当这个类型需要显示为字符串时,
//Go语言会自动寻找 String() 方法并进行调用。
func (c WeekDay) String() string{
	switch c {
		case Sunday:
			return "Sunday"
	}
}
  1. 定义类型别名的写法为
//type TypeAlias = Type
// 将NewInt定义为int类型
type NewInt int
// 将int取一个别名叫IntAlias
type IntAlias = int
  1. godoc 工具会从 Go 程序和包文件中提取顶级声明的首行注释以及每个对象的相关注释,并生成相关文档

第三章 go语言容器

1.Go语言数组

var a [3]int             // 定义三个整数的数组
fmt.Println(a[0])        // 打印第一个元素
fmt.Println(a[len(a)-1]) // 打印最后一个元素
// 打印索引和元素
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}
// 仅打印元素
for _, v := range a {
    fmt.Printf("%d\n", v)
}

2.用一组值来初始化数组

var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
//在数组的定义中,如果在数组长度的位置出现“...”省略号,
//则表示数组的长度是根据初始化值的个数来计算
q := [...]int{1, 2, 3}
//数组的长度是数组类型的一个组成部分,
//因此 [3]int 和 [4]int 是两种不同的数组类型,
//数组的长度必须是常量表达式,
//因为数组的长度需要在编译阶段确定。
q := [3]int{1, 2, 3}
q = [4]int{1, 2, 3, 4} // 编译错误:无法将 [4]int 赋给 [3]int
//如果两个数组类型相同(包括数组的长度,数组中元素的类型)的情况下,
//我们可以直接通过较运算符(== 和!=)来判断两个数组是否相等
  1. Go语言多维数组
// 声明一个二维整型数组,两个维度的长度分别是 4 和 2
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化数组中索引为 1 和 3 的元素
array = [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化数组中指定的元素
array = [4][2]int{1: {0: 20}, 3: {1: 41}}
  1. 切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型
var a  = [3]int{1, 2, 3}
fmt.Println(a, a[1:2])
// 声明字符串切片
var strList []string
// 声明整型切片
var numList []int
// 声明一个空切片
var numListEmpty = []int{}
//如果需要动态地创建一个切片,可以使用 make() 内建函数,
a := make([]int, 2)
b := make([]int, 2, 10)
fmt.Println(a, b)
fmt.Println(len(a), len(b))

//用append为切片添加元素
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
a = append([]int{0}, a...) // 在开头添加1个元素

// 在第i个位置插入x
a = append(a[:i], append([]int{x}, a[i:]...)...) 
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

a = []int{1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素

//声明一个二维切片
var slice [][]int
//为二维切片赋值
slice = [][]int{{10}, {100, 200}}
  1. 在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”
  2. map
var mapname map[keytype]valuetype
noteFrequency := map[string]float32 {
"C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,
"G0": 24.50, "A0": 27.50, "B0": 30.87, "A4": 440}
//使用 delete() 函数从 map 中删除键值对
delete(map,)
  1. 当发生并发时, map会报错, sync.Map 不能使用 map 的方式进行取值和设置等操作,而是使用 sync.Map 的方法进行调用,Store 表示存储,Load 表示获取,Delete 表示删除。
  2. 初始化列表: 变量名 := list.New()
  3. 通过 var 关键字声明初始化 list : var 变量名 list.List
  4. 在列表中插入元素
l := list.New()
l.PushBack("fist")
l.PushFront(67)
// 尾部添加后保存元素句柄
element := l.PushBack("fist")
// 在fist之后添加high
l.InsertAfter("high", element)
// 在fist之前添加noon
l.InsertBefore("noon", element)
// 使用
l.Remove(element)
//遍历
for i := l.Front(); i != nil; i = i.Next() {
    fmt.Println(i.Value)
}

11.指针、切片、映射、通道、函数和接口的零值则是 nil。Go语言中的 nil 和其他语言中的 null 有很多不同点

//nil 标识符是不能比较的
nil == nil  //报错
//nil 不是关键字或保留字
var nil = errors.New("my god")

//不同类型的 nil 值占用的内存大小可能是不一样的
var p *struct{}
fmt.Println( unsafe.Sizeof( p ) ) // 8

12.new 函数不仅仅能够为系统默认的数据类型,分配空间,自定义类型也可以使用 new 函数来分配空间

type Student struct {
   name string
   age int
}
var s *Student
//如果我们不使用 new 函数为自定义类型分配空间就会报错
s = new(Student) //分配空间
s.name ="dequan"
fmt.Println(s)
  1. make 也是用于内存分配的,但是和 new 不同,它只用于 chan、map 以及 slice 的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

第四章 流程控制

  1. Go语言中的循环语句只支持 for 关键字,而不支持 while 和 do-while 结构
sum := 0
for i := 0; i < 10; i++ {//左花括号{必须与 for 处于同一行。
    sum += i
}
//写法2
for {
    sum++
    if sum > 100 {
        break
    }
}
//初始语句可以被忽略,但是初始语句之后的分号必须要写
step := 2
for ; step > 0; step-- {
    fmt.Println(step)
}
//只有一个循环条件的循环
for i <= 10 {
    i++
}
  1. Go语言改进了 switch 的语法设计,case 与 case 之间是独立的代码块,不需要通过 break 语句跳出当前 case 代码块以避免执行到下一行

第五章 函数

  1. 普通函数声明(定义)
func 函数名(形式参数列表)(返回值列表){
    函数体
}
func hypot(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}
  1. Go语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误
//connectToNetwork 返回两个参数,conn 表示连接对象,err 返回错误信息。
conn, err := connectToNetwork()
  1. 函数也是一种类型,可以和其他类型一样保存在变量中
func fire() {
    fmt.Println("fire")
}
func main() {
    var f func()
    f = fire
    f()
}
  1. 定义一个匿名函数
func(参数列表)(返回参数列表){
    函数体
}
  1. go语言支持闭包
  2. 可变参数是指函数传入的参数个数是可变的,为了做到这点,首先需要将函数定义为可以接受可变参数的类型
func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}
//任意类型的可变参数
func Printf(format string, args ...interface{}) {
    // ...
}
  1. 当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出)
// 将defer放入延迟调用栈
    defer fmt.Println(1)
    defer fmt.Println(2)
    // 最后一个放入, 位于栈顶, 最先调用
    defer fmt.Println(3)
    //输出结果是3 2 1 
  1. 一般使用延迟执行语句在函数退出时释放资源,释放文件句柄
  2. 自定义一个错误
var err = errors.New("this is an error")
  1. 手动触发宕机, 在宕机时会触发延迟执行语句
package main
func main() {
    panic("crash")
}
  1. 单元(功能)测试: 在同一文件夹下创建两个Go语言文件,分别命名为 demo.go 和 demt_test.go, 执行测试命令: go test -v
  2. 性能(压力)测试 : go test -bench="."

第六章 结构体

  1. 结构体的定义格式如下
type 类型名 struct {
    字段1 字段1类型
    字段2 字段2类型
    …
}
//基本实例化格式如下
var ins T

type Point struct {
    X int
    Y int
}
var p Point
p.X = 10
p.Y = 20

//创建指针类型的结构体
ins := new(Point)
//对结构体进行&取地址操作时,视为对该类型进行一次 new 的实例化操作
ins := &Point{}
  1. 初始化结构体的成员变量
type People struct {
    name  string
    child *People
}
relation := &People{
    name: "爷爷",
    child: &People{
        name: "爸爸",
        child: &People{
                name: "我",
        },
    },
}
  1. 使用多个值的列表初始化结构体
type Address struct {
    Province    string
    City        string
    ZipCode     int
    PhoneNumber string
}
addr := Address{
    "四川",
    "成都",
    610000,
    "0",
}
  1. 初始化匿名结构体
msg := &struct {  // 定义部分
        id   int
        data string
    }{  // 值初始化部分
        1024,
        "hello",
    }
  1. 为结构体添加方法
type Bag struct {
    items []int
}
//(b*Bag) 表示接收器,即 Insert 作用的对象实例
func (b *Bag) Insert(itemid int) {
    b.items = append(b.items, itemid)
}
  1. 在一个结构体中对于每一种数据类型只能有一个匿名字段
type innerS struct {
    in1 int
    in2 int
}
type outerS struct {
    b int
    c float32
    int // anonymous field
    innerS //继承
}
  1. 内嵌结构体的字段名是它的类型名
  2. 内嵌结构体实现对象特性,可以自由地在对象中增、删、改各种特性
  3. GC 是自动进行的,如果要手动进行 GC,可以使用 runtime.GC() 函数
  4. 当方法作用于非指针接收器时,Go语言会在代码运行时将接收器的值复制一份,在非指针接收器的方法中可以获取接收器的成员值,但修改后无效。

第七章 接口

  1. 接口的声明方式
type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2}
  1. 如果一个任意类型 T 的方法集为一个接口类型的方法集的超集,则我们说类型 T 实现了此接口类型。T 可以是一个非接口类型,也可以是一个接口类型。
  2. 实现关系在 Go语言中是隐式的。两个类型之间的实现关系不需要在代码中显式地表示出来。Go语言中没有类似于 implements 的关键字。 Go编译器将自动在需要的时候检查两个类型之间的实现关系。
  3. 实现接口
package main
import (
    "fmt"
)
// 定义一个数据写入器
type DataWriter interface {
    WriteData(data interface{}) error
}
// 定义文件结构,用于实现DataWriter
type file struct {
}
// 实现DataWriter接口的WriteData方法
func (d *file) WriteData(data interface{}) error {
    // 模拟写入数据
    fmt.Println("WriteData:", data)
    return nil
}
func main() {
    // 实例化file
    f := new(file)
    // 声明一个DataWriter的接口
    var writer DataWriter
    // 将接口赋值f,也就是*file类型
    // writer 是一个接口,且 f 已经完全实现了 DataWriter() 的所有方法,因此赋值是成功的。
    writer = f
    // 使用DataWriter接口进行数据写入
    writer.WriteData("data")
}
  1. 实现接口的函数名,或参数类型 不一致会导致报错
  2. 当一个接口中有多个方法时,只有这些方法都被实现了,接口才能被正确编译并使用。
  3. 一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
  4. 类型断言
var x interface{}
    x = 10
    //如果不接收第二个参数,断言失败时会直接造成一个 panic。如果 x 为 nil 同样也会 panic
    value, ok := x.(int)
    //10, true
    fmt.Print(value, ",", ok)
    
//第二种断言方式
func getType(a interface{}) {
    switch a.(type) {
    case int:
        fmt.Println("the type of a is int")
    case string:
        fmt.Println("the type of a is string")
    case float64:
        fmt.Println("the type of a is float")
    default:
        fmt.Println("unknown type")
    }
}
  1. 空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值
  2. 空接口的内部实现保存了对象的类型和指针。使用空接口保存一个数据的过程会比直接用数据对应类型的变量保存稍慢。因此在开发中,应在需要的地方使用空接口,而不是在所有地方使用空接口。
  3. 当空接口中保存有动态类型的值时,运行时将触发错误
  4. 类型不同的空接口间的比较结果不相同
  5. error 接口有一个签名为 Error() string 的方法,所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述,在使用 fmt.Println 打印错误时,会在内部调用 Error() string 方法来得到该错误的描述。
type error interface {
    Error() string
}

第八章 Go语言包(package)

  1. Go 语言的包与文件夹一一对应,所有与包相关的操作,必须依赖于工作目录(GOPATH)。
  2. 任何源代码文件必须属于某个包。源码文件的第一行有效代码必须是 package pacakgeName 语句,通过该语句声明自己所在的包。
  3. 包可以定义在很深的目录中,包的定义是不包括目录路径的,但是包的引用一般是全路径引用
  4. 标准包的源码位于 $GOROOT/src/ 下面,标准包可以直接引用。自定义的包和第三方包的源码必须放到 $GOPATH/src 目录下才能被引用。
  5. 包的引用路径有两种写法,一种是全路径,另一种是相对路径。
//全路径
import "lab/test"
//相对路径
import "../a"
//别名引用方式
import F "fmt"
//省略方式:相当于把包 fmt 的命名空间直接合并到当前程序的命名空间中,
//使用 fmt 包内可导出元素可以不用前缀“fmt.”,直接引用
import . "fmt"
//使用标准格式引用包,但是代码中却没有使用包,编译器会报错。
//如果包中有 init 初始化函数,则通过 import _ "packageName" 这种方式引用包,
//仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错
import _ "fmt"
  1. 结构体内的小写字母开头的成员不能被外部直接访问

  2. GOPATH 是 Go语言中使用的一个环境变量,它使用绝对路径提供项目的工作目录。

  3. 执行 go env 指令,将输出当前 Go 开发包的环境变量状态。
  4. 在 GOPATH 指定的工作目录下,代码总是会保存在 $GOPATH/src 目录下。在工程经过 go build、go install 或 go get 等指令后,会将产生的二进制可执行文件放在 $GOPATH/bin 目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg 下。
  5. 在 Go语言里,允许我们将同一个包的代码分隔成多个小块来单独保存,只需要将这些文件放在同一个目录即可。

  6. 包名可能会有名字冲突。最简单的方法就是确保 GOPATH 里包含的每个路径都是唯一的
  7. Go语言提供了非常方便的文档化工具 godoc,可以在命令行显示包的文档和函数,也可以作为一个 Web 服务器启动
  8. 当导入一个包时,它所有的 init() 函数就会被执行。
  9. 一个目录下的同级文件归属一个包。
  10. 包名为 main 的包为应用程序的入口包,编译源码没有 main 包时,将无法编译输出可执行的文件。
  11. 如果想在一个包里引用另外一个包里的标识符(如类型、变量、常量等)时,必须首先将被引用的标识符导出,将要导出的标识符的首字母大写就可以让引用者可以访问这些标识符了

  12. 在被导出的结构体或接口中,如果它们的字段或方法首字母是大写,外部可以访问这些字段和方法
  13. 默认导入的写法
//单行导入
import "包1"
//多行导入
import(
    "包1"
    "包2")
  1. 互斥锁 Mutex:当一个协程正在处理 a 时将 a 锁定,其它协程需要等待该协程处理完成并将 a 解锁后才能再进行操作,也就是说同时处理 a 的协程只能有一个
go func(idx int) {
    lock.Lock()
    defer lock.Unlock()
    a += 1
    fmt.Printf("goroutine %d, a=%d\n", idx, a)
}(i)
  1. 写操作的锁定和解锁分别是func (*RWMutex) Lock和func (*RWMutex) Unlock;读操作的锁定和解锁分别是func (*RWMutex) Rlock和func (*RWMutex) RUnlock, 当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定
//创建读写锁
var rw sync.RWMutex
rw.RLock()
fmt.Printf("goroutine %d 进入读操作...\n", n)
v := count
fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
rw.RUnlock()
  1. 处理超大整数可以用big包
	big1, _ := new(big.Int).SetString("1000", 10)
    fmt.Println("big1 is: ", big1)
    big2 := big1.Uint64()
    fmt.Println("big2 is: ", big2)

第九章 并发

  1. Go 程序会智能地将 goroutine 中的任务合理地分配给每个 CPU。
go 函数名( 参数列表 )
//匿名函数
go func( 参数列表 ){
    函数体
}( 调用参数列表 )
  1. 调整并发的运行性能
// runtime.NumCPU() 查询 CPU 数量,并使用 runtime.GOMAXPROCS() 函数进行设置
runtime.GOMAXPROCS(runtime.NumCPU())
  1. 并发不是并行。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。
  2. goroutines 意味着并行(或者可以以并行的方式部署),coroutines 一般来说不是这样的,goroutines 通过通道来通信
  3. 如果说 goroutine 是 Go语言程序的并发体的话,那么 channels 就是它们之间的通信机制。一个 channels 是一个通信机制,它可以让一个 goroutine 通过它给另一个 goroutine 发送值信息。每个 channel 都有一个特殊的类型,也就是 channels 可发送数据的类型。一个可以发送 int 类型数据的 channel 一般写为 chan int。

你可能感兴趣的:(go)