《Go语言实践》笔记

打包和工具链

包的惯例
  1. 必须在头部声明自己的包名
  2. 每个包全部都在一个单独的目录里。一个目录不存在多个包、一个包不会存在多个目录。
  3. 包名应保持小写、简洁
main包
  1. 编译程序会将这种名字的包编译为二进制可执行文件。 可执行程序必须有一个main的包
  2. 必须存在一个main()函数

导入

导入使用关键字 import;导入的方式分为以下三种:

  • 远程导入import "github.com/spf13/viper"
  • 命名导入import myviper "mylib/viper"。一般用于解决冲突或者别名
  • 空白标识符导入import _ "db/driver/mysql"。一般用于初始化代码才会如此

init函数

  1. 每个包可以包含多个init()函数。
    1. 支持单个文件多个
    2. 支持多个文件多个
  2. main()之前运行

PS - 部分场景:在 init()中初始化向某个插件注册功能。可执行程序处引用包含init()的文件、包

执行顺序

  1. import subPkg. 引入包
  2. const. 常量定义
  3. var. 变量定义
  4. subPkg.init(). 引入包执行 init
    1. 以导入声明的顺序初始化
  5. init(). 优先包含 main函数的 init(), 然后其他 init()
  6. main(). 当前main

常见工具

  • go env.查看环境变量
  • go list.查看依赖包
  • go build.编译包和依赖项
    • go build -race竞争检测器来编译并执行
  • go run.编译并运行Go程序
  • go vet.代码错误检查
  • go fmt.代码格式化
  • go get.下载 && 安装软件包和依赖(远程)
  • go install.编译 && 安装软件包和依赖项(本地)

第三方依赖管理工具

常量

基本语法

const 常量名[ 类型] = 值

特点总结

  1. 一旦定义后无法修改
  2. "类型"只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
  3. "类型"不填写时, 常量会根据 “值” 自动判断类型
  4. “值” 必须在编译时就能够确定;不可以是 “自定义函数”, 但可以是内置函数.

使用示例

const (
	Pi = 3.1415926
    ma, mb = "string", true
    fa = len(ma)
    na = 1e9
    nb = 1/Pi
)

变量

基本语法

var 变量名[ [*]类型] = 值

特点总结

短变量声明
  1. 在多个赋值的时候,如果存在已经存在的会变成赋值
  2. 在函数内部可以简单的使用:=在明确类型的地方代替 var声明。

PS: 函数外的每个语句必须使用(var,func 等关键字)

k := 3
ma, mb := "string", true

使用示例

package main

import (
	"fmt"
)

var sa int     // 仅声明
var sb int = 3 // 声明 且 初始化
var sc = 3     // 初始化 且 自动推导类型

var ma, mb, mc = "string", true, 1 // 从左至右批量 初始化 且 自动推导类型

// 代码块中赋值
var (
	ToBe   bool   = false
	MaxInt uint64 = 1<<64 - 1
)

var s = "string" // 有效范围示例

func main() {
	s := 1
	fmt.Println(s)
    
    x, y := 0, 1
    x, y = y, x	// x:1 y:0 多重赋值可以轻松的交换数据
}

数据类型

类型说明

bool 

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // int8
	下方三种写法等效:
	var ch byte = 'A'	// 单引号包裹字符串
	var ch byte = 65	// ASCII码表 10进制
	var ch byte = '\x41'// ASCII码表 16进制 - \x
	
	
rune // int32 的别名
     // 代表一个Unicode码

float32 float64

complex64 complex128

零值

数值类型为 `0`,
布尔类型为 `false`,
字符串为 `""`(空字符串)

类型转换

i := 42
f := float64(i)
u := uint(f)

数组、切片、映射

数组

底层特点
  • 长度固定
  • 元素类型相同
  • 占用内存连续分配(元素间相邻)
  • 访问任意下标元素的速度相同。(原因是内存连续 + 类型相同 可以定位指定偏移量)
  • 函数传参时通过 值传递
使用示例
// 简单的声明 - 初始化为 "零值"
var array1 [5]int
fmt.Println(array1) // [0 0 0 0 0]

// 赋值初始化 - 初始化所有数值
var array2 = [5]int{10, 20, 30, 40, 50}
fmt.Println(array2) // [10 20 30 40 50]

// 自动推导长度
array3 := [...]int{10, 20, 30, 40, 50}
fmt.Println(array3) // [10 20 30 40 50]

// 赋值初始化 - 初始化指定下标
array4 := [5]int{1: 10, 2: 20}
fmt.Println(array4) //[0 10 20 0 0]

// 数组指针
array5 := [5]*int{0: new(int), 2: new(int)}
*array5[0] = 10
*array5[1] = 20
fmt.Println(array5) // [0xc000010148 <nil> 0xc000010180 <nil> <nil>

// 复制
var array6a [5]string
var array6b := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}

array6a = array6b    // 长度、元素类型 相同才可以允许相互赋值
fmt.Println(array6a) // [Red Blue Green Yellow Pink]

// 指针 复制
// 指针数组 相互赋值 两边的指针依然是 --- "指向 相同的内存地址"

切片

底层特点
  • 是一种数据结构。指针、长度、容量(指针指向底层数组首个元素内存地址
  • 长度动态变更。通过 append扩容、通过二次切片缩小
  • 函数传参return返回时 复制切片本身,不涉及底层数组
  • 容量必定大于等于长度
  • 底层数组元素小于1000时,扩容系数为2;大于1000时扩容系数为1.25

PS: 解决在函数中产生扩容后 “新切片指针” 和 "旧切片指针"指向的底层数组不同的问题

// 分配包含 100 万个整型值的切片
slice := make([]int, 1e6)

// 将 slice 传递到函数 foo
slice = foo(slice)

// 函数 foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
    ...
    return slice
}
使用示例
// 声明长度 默认容量
slice := make([]string, 5) // len:5 cap:5

// 声明长度 声明容量
slice := make([]string, 5, 6) // len:5 cap:6

// 初始化 所有值 自动推断长度、容量
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"} // len:5 cap:5

// 初始化 指定值 自动推断长度、容量
slice := []string{99: ""}	// len:100 cap:100

// 指针说明
slice := []int{10, 20, 30, 40, 50}
newSlice := slice[1:3]			// 此处指针后移至 索引"1"处; len:2 cap:4
newSlice[1] = 35 				// 两个切片指向同一个底层数组,因此修改是同步的
newSlice = append(newSlice, 6) 	// 当底层容量大于长度时(未扩容) 指向的还是同一个底层数组
fmt.Println(slice, newSlice) 	// [10 20 35 6 50] [20 35 6]

// 针对 ·当底层容量大于长度时(未扩容) 指向的还是同一个底层数组· 的问题可以通过第三个参数解决
newSlice := slice[1:3:3] // 指定容量 slice[i:x:y] len:x-i cap:y-i

// 扩容后
newSliceAdd := append(slice, 5)
// 此处有两个特别的:
// 1. 由于本身切片属于隐性指针,所以打印内存地址的时候不需要加上 &slice
// 2. 这里的指针指向的是首个元素的内存地址, 所以起始位置不一样的也会不同
// 扩容其实就是创建一个更大的数组,然后把数据复制过去
fmt.Printf("old指针地址:%p new指针地址:%p \n", slice, newSliceAdd) //old指针地址:0xc00000c3c0 new指针地址:0xc00001c1e0

// 切片合并
s1 := []int{1, 2}
s2 := []int{3, 4}
fmt.Println(append(s1, s2...)) // [1 2 3 4]

// 切片迭代
for index, val := range slice {
    // index, val 属于值传递 && 有效范围在代码块里面
    fmt.Printf("%v = %v\n", index, val)
}
nil切片和空切片

不论nil 切片还是空切片,都没有分配任何存储空间。

nil 切片:声明&不初始化

var slice []int
fmt.Printf("val:%v isnil:%v type:%T len:%d, cap:%d\n", slice, nil == slice, slice, len(slice), cap(slice))

// val:[] isnil:true type:[]int len:0, cap:0

空切片:声明&初始化空切片

// 两种方式等价
slice := make([]int, 0)
slice := []int{}
fmt.Printf("val:%v isnil:%v type:%T len:%d, cap:%d\n", slice, nil == slice, slice, len(slice), cap(slice))

// val:[] isnil:false type:[]int len:0, cap:0

映射

底层特点
  • 映射是一个存储键值对的无序集合. (底层为散列。TODO::书中没有详细备注,暂时理解为hashmap)
  • 迭代时是无序的,可能每次的迭代顺序是不一样的
  • 迭代时当类型未定义时返回对应类型的零值
  • nil映射无法直接赋值
  • 函数传参时 会用很小的代价复制,不涉及底层数组(TODO::后面详细看看为啥)
使用示例
// 创建一个映射
colors := make(map[string]string)

// 创建一个映射 并初始化
colors := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

colors["read"] = "#cccc"
fmt.Println(colors)

m := make(map[string]int)

// 添加 & 修改
m["Answer"] = 42
fmt.Println("The value:", m["Answer"])
m["Answer"] = 48
fmt.Println("The value:", m["Answer"])

for index := range m {
	fmt.Println("for index:", index)
}

// 删除
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])

// 检测某个键是否存在 -- 不存在 exists 为 false
value, exists := m["Answer"]
fmt.Println("The value:", value, "Present?", exists)
nil映射

nil 映射:声明&不初始化nil映射无法直接赋

var dict map[string]int
fmt.Printf("val:%v isnil:%v type:%T\n", dict, nil == dict, dict)

// val:map[] isnil:true type:map[string]int

dict["aaa"] = 1 // 会报错:assignment to entry in nil map

类型系统

自定义类型

结构类型
声明和使用
// 声明一个结构类型
type Man struct {
	Name   string
	Age    int
	Height float64
	int // 匿名字段. 必须不重复. 访问时为字段类型名
}

// 使用结构类型声明变量 & 初始化为零值
var zhangsan Man

// 使用结构类型声明变量 & 初始化信息
lisi := Man{
	Name: "李四",
}

// 注意零值(默认值)
fmt.Println(zhangsan == Man{}, lisi) // true {李四 0 0}
内嵌结构体
type Admin struct {
	// 内嵌的结构体
	User  Man
	Level int
}

teacher := Admin{
    User: Man{
    	Name: "李四",
    },
    Level: 127,
}
命名类型 - 非结构体类型

就是基于已知类型来声明自定义类型

类似于别名的类型和原类型是不一样的

type myint int

var data myint
data = int(6) // 这里编译会报错;因为 myint 不是 int 类型

data := myint(6)

方法

如果一个函数有接收者,这个函数就被称为方法

接收者类型

它的作用是将函数与接收者的类型绑在一起一般情况下会使用其中一种,不会混用

  • 值接收者。调用时会使用这个值的一个副本来执行
  • 指针接收者。调用时能够修改其接收者实际指向的值
指针重定向

指针接收者的方法被调用时,接收者( 声明对应类型的值 )既能为(隐性转换为 &p)又能为指针

// 普通函数
func manChangeName(man *Man, name string) {
    man.Name = name
}

m := Man{}
manChangeName(m, "new") // 编译错误!因为类型不对,函数定义传指针
manChangeName(&m,"new") // OK

// 方法
func (man *Man) changeName(name string) {
    man.Name = name
    // 对于下方的 "ma", 由于返回的就是指针,所以可以调用
    // 对于下方的 "mv", 这里Go隐性转换了访问方式 (&p).Name = name
}

ma := &Man{}			// 这里返回的是指针
ma.changeName("new")	// OK

mv := Man{}				// 这里返回的是值, 但为什么可以调用指针接收者的方法?见上面备注
mv.changeName("new")	// OK

值接收者的方法被调用时,接收者( 声明对应类型的值 )既能为又能为指针(*隐性转换为 p)

// 普通函数
func manGetName(man Man) string {
    return man.Name
}

m := Man{Name:"张三"}
manGetName(m ) // OK
manGetName(&m) // 编译错误!因为类型不对,函数定义传指针

// 方法
func (man *Man) getName() string {
    return man.Name
    // 对于下方的 "ma", 这里Go隐性转换了访问方式 (*p).Name . 指向底层数据
}

ma := &Man{}	// 这里返回的是指针,但为什么可以调用值接收者的方法?见上面备注
ma.getName()	// OK

mv := Man{}		// 这里返回的是值
ma.getName()	// OK

PS:所以要防止数据的指针传递时不小心修改到原始数据

m := &Man{Name:"张三"}
n := m

n.changeName("李四")
fmt.Println(n.getName(), m.getName()) // 李四 李四. !!!修改到原来的数据了!!!

fmt.Printf("n内存地址:%p m内存地址:%p \n", &*m, &*n) // 一样的

类型的本质

保持传递的一致性很重要。这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。

PS:通过了解本质来构建正确的代码。

内置类型

当对内置的一些类型( string, int … )进行增加或者删除的时候,会创建一个新值

// 保持传递的一致性 + 返回新值
func uTrim(s string) string

// 关注这个值的本质 + 返回新值
func uStrLen(s string) int
引用类型

通过复制来传递一个引用类型的值副本,本质上就是共享底层数据

Go语言中存在着引用类型有如下几种类型:切片 映射 通道 接口 函数

PS:从技术细节上说,字符串也是一种引用类型

当创建引用类型变量时,创建的变量称为 标头(header)值;它是为了复制而设计的,它包含了一个指向实际底层数据的指针

// 保持传递的一致性 + 复制传递(根本不需要传指针)
func uSliceMerge(s []int, a []int) []int
结构类型

这里的章节没看懂,下面内容摘自网络;(TODO::后续补充 章节 5.3.3)

  • 原始类型,指的是内建基本类型,比如int,byte,string等
  • 非原始是指用户自定义类型,比如用户自定义的 struct
  • 因为内建类型本身大小很小,所以传值和传引用(应该是传指针)区别不大,而自定义的struct 往往可能会包含很多字段,在比较大的情况下,传(引用)指针会避免值拷贝,从而提高效率。
  • 不要一味的进行传(引用)指针,还要考虑逃逸分析问题,过多的引用会加大gc负担
  • go中默认值传递,闭包中特殊处理,会引用可见域中的变量,而不是值传递

是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质

接口类型

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现

空接口

指定了零个方法的接口值被称为 *空接口:( **空接口可保存任何类型的值 *)

interface{}

// 接收 任意类型的 参数
func Decode(v interface{}) error {
    // ...
}
底层原理

TODO::itable 什么的还是要最后确认后才可以确定;下面是初略的理解。

实体类型(用户定义的类型)赋值给接口时,接口通过两部分数据来保存对应的信息:

  • 内部表 iTable 的地址
    • 实体对象 类型. (值类型、指针类型)
    • 方法集。内部映射了 接口类型声明的方法 - 实体对象方法地址(TODO::看报错像是这样的)
  • 实体对象 值的地址

PS:实体对象 描述不正确,是自己瞎起的。就是 实体类型 初始化后

定义方法
type Man interface {
    setName(n string) string
    getName() string
    online()
}
使用场景

直接赋值;获取实体类型的值和接口需要的方法集

// 假设 管理员角色 包含 人的结构体信息
man Man := Manager{Name:"张三", Online:false}

man.online() // 执行用户上线
man.setGuestAcl(....) // !!!这里要特别注意,此处是不可能调用到的!!!

类型约束;使用函数的时候进行类型约束,是否实现对应的方法集

man := Manager{Name:"张三", Online:false}


func resetDefaultUserName(m Man) {
    _ := m.setName(DEFAULT_MAN_NAME)
    man.setGuestAcl(....) // !!!这里要特别注意,此处也是不可能调用到的!!!
}
方法集规则

方法集定义了一组关联到实体类型的值或者指针的方法。(明确一点 int 和 *int 不是一个类型

实体类型定义方法时使用的接收者的类型决定了这个方法是关联到值,还是关联到指针,还是两个都关联

接收者类型值类型
指针接收者 (v *T)指针类型
值接收者(v T)指针类型 & 值类型

PS:特殊场景

package main

import "fmt"

// 声明一个 命名类型
type duration int

// 指针接收者类型
func (d *duration) pretty() string {
	return fmt.Sprintf("Duration: %d", *d)
}

func main() {
	// 默认 Go 会尽可能的进行 指针重定向;但已下会报错查找不到
	duration(42).pretty()
	
	// 这样却可以,因为隐性的转换了
	d := duration(42)
	d.pretty()
}

嵌入类型

  • 默认访问最外层 字段、方法
  • 最外层可以重新声明嵌入类型定义的 字段、方法
  • 可以通过层级独立访问内嵌类型的方法

并发

goroutine

Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes, CSP)的范型(paradigm)。 CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。

竞争状态

如果两个或者多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态,这种情况被称作竞争状态(race candition)。

通过 go build -race( -race 使用竞争检测器)来编译执行发现问题

共享资源锁

原子函数

以很底层的加锁机制来同步访问整型变量和指针 。强制同一时刻只能有一个 goroutine 运行并完成指定操作。

sync/atomic

import "sync/atomic"

// 类型共六种:int32, int64, uint32, uint64, uintptr, unsafe.Pinter
// 操作共五种:增减, 比较并交换, 载入, 存储,交换

// 增减 => AddT => AddInt64
func AddInt32(addr *int32, delta int32) (new int32)

// 载入 => LoadT => LoadInt64
// 存储 => StoreT => StoreInt64
func LoadInt32(addr *int32) (val int32)
func StoreInt32(addr *int32, val int32)

// 交换 => SwapT => SwapInt64
func SwapInt32(addr *int32, new int32) (old int32)

// 比较并交换(就是 CAS) => CompareAndSwapT => CompareAndSwapInt64
func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
互斥锁

互斥锁用于在代码上创建一个临界区,保证同一时间只有一个 goroutine 可以执行这个临界区代码。

当手动调用runtime.Gosched()强制将当前 goroutine 退出当前线程后,调度器会再次分配这个 goroutine 继续运

import "sync"

var mutex sync.Mutex

mutex.Lock()
// ...临界区内容...
mutex.Unlock()

通道

可通过通道共享的内容:内置类型命名类型结构类型引用类型的值或者指针

// 无缓冲的通道
unbuffered := make(chan int)
// 有缓冲的通道
buffered := make(chan string, 10)

// 发送
buffered<-true
// 读取
val := <-buffered
val, isclose := <-buffered

// 关闭通道
close(buffered)
无缓冲的通道
  • 发送者:阻塞等待
  • 接收者:阻塞等待
  • 同步方式:同步发送。保证发送、接收的goroutine会在同一时间进行数据交换
有缓冲的通道
  • 发送者:通道没有可用缓冲区容纳被发送的值时,发送动作阻塞
  • 接收者: 通道中没有要接收的值时,接收动作阻塞
  • 同步方式:异步发送。不保证同时交换数据
close
  • 当通道关闭后, goroutine 依旧可以从通道接收数据,但是不能再向通道里发送数据。(保证数据不会丢失)
  • 从一个已经关闭没有数据的通道里获取数据,总会立刻返回,并返回一个通道类型的零值。(不能通过值判断是否有数据,而应该通过可选标志位获取通道状态)
  • 这个方法应该只由发送者调用, 而不是接收者。却调用的channel应该是双向的或者只写(chan<- Type)
select

语法特点:

  • 表达式会被解析求值。求值顺序:自上而下、从左到右
  • 多个同时到达则随机选择一个完成(CSP理论),默认default

iota

让编译器为每个常量复制相同的表达式,直到声明区结束,或者遇到一个新的赋值语句。

  • 只能在常量的表达式中使用
  • 从0开始,每次递增1
  • 每次 const 出现时,都会让 iota 初始化为0
  • 同一代码块中允许多次使用 iota (不会重新计数),通过"="重新赋值后需要使用 iota 继续应用表达式
  • 支持自定义枚举类型
    • 也就是语言基本类型和对应的命名类型
    • 注意 bytestrings
  • 可通过 “_” 跳过值
  • 可通过 “=” 赋值其他值

声明示例:

const A = iota	 // 0
const (
	B = iota     // 0 -- 每次 const 出现时,都会让 iota 初始化为0 
	C            // 1
    D = "A"		 // A
	E			 // A
	F			 // A
)

const A = iota // 0
const (
	B = iota     // 0 -- 每次 const 出现时,都会让 iota 初始化为0
	C            // 1 -- 从0开始,每次递增1
    D = "A"		 // A -- 可通过 "=" 赋值其他值
	E = iota	 // 3 -- 同一代码块中允许多次使用 iota,通过"="重新赋值后需要使用 iota 继续应用表达式
	F			 // 4
)

type Stereotype int
const (
	TypicalNoob Stereotype = 2 * iota // 2 * 0 => 0 -- 支持自定义枚举类型
    TypicalHipster                	  // 2 * 1 => 2
    TypicalUnixWizard                 // 2 * 2 => 4
    TypicalStartupFounder             // 2 * 3 => 6
)

const (
    _ = iota	// 0 -- 可通过 "_" 跳过值
    _			// 1
    V2			// 2
    _			// 3
    V4			// 4
)

常见用法:

type ByteSize float64

const (
	_           = iota
	KB ByteSize = 1 << (10 * iota) // 1 << (10*1)
	MB                             // 1 << (10*2)
	GB                             // 1 << (10*3)
	TB                             // 1 << (10*4)
	PB                             // 1 << (10*5)
	EB                             // 1 << (10*6)
	ZB                             // 1 << (10*7)
	YB                             // 1 << (10*8)
)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值