运行和解析
go run 命令已包含了编译和运行。它会先在一个临时目录编译程序,然后执行完后清理掉.
如果在run的后面加上 --work参数来查看临时目录。
go run --work main.go
也可以通过go build命令来编译代码,执行后会生成一个可以执行文件,通过./XX直接执行
交叉编译
# 要去linux下执行
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
# 要去Mac下执行
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go
# 要去win下执行
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
CGO_ENABLED : CGO 表示golang中的工具,CGO_ENABLED 表示CGO禁用,交叉编译中不能使用CGO的
GOOS
目标平台
mac 对应 darwin
linux 对应 linux
windows 对应 windows
GOARCH
目标平台的体系架构【386,amd64,arm】, 目前市面上的个人电脑一般都是amd64架构的
386 也称 x86 对应 32位操作系统
amd64 也称 x64 对应 64位操作系统
arm 这种架构一般用于嵌入式开发。 比如 Android , IOS , Win mobile , TIZEN 等
变量类型
:整型、浮点型、布尔型
变量声明:
标准声明 var 变量名称 变量类型 不需要分号
批量声明 var (变量名称 变量类型
变量名称 变量类型
变量名称 变量类型
… …)
变量初始化 var 变量名 类型 = 表达式 ,其中整型和浮点型变量默认值为0,布尔型变量默认为false,
初始化多个变量 var 变量名,变量名… = 表达式,表达式…
类型推导 var 变量名 = 表达式 ,编译器会根据表达式的值来推导变量名的类型并完成初始化
短变量声明 变量名 := 表达式
匿名变量 _一个下划线来表示
注意事项:
函数外的每个语句都必须以关键字开始(var、const、func等)
:=不能使用在函数外。
_多用于占位,表示忽略值。
常量 const
iota 常量计数器,在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次
定义数量级 << 左移操作
多个iota定义在一行
基本类型
bool (布尔)
数值类型 分为有符号数和无符号数,有符号数可以用来表示负数,不同的位数代表它们实际存储占用空间,以及取值的范围
int8、int16、int32、int64、int
uint8、uint16、uint32、uint64、uint
float32, float64
complex64, complex128
byte
rune
string
类型转换,对显式类型非常严格,没有自动类型提升或转换
循环条件
For 循环语句用于重复执行一段代码,是 Go 中唯一可用的循环
for initialisation; condition; post {
}
break语句用于在 for 循环完成正常执行之前突然终止 for 循环,并将控件移动到 for 循环之后的代码行。
continue语句用于跳过 for 循环的当前迭代。在 continue 语句之后出现在 for 循环中的所有代码都不会在当前迭代中执行。循环将继续进行下一次迭代。
if else 是一个具有布尔条件的语句,如果该条件的计算结果为真 ,它将执行一段代码true。如果条件评估为 ,它会执行一个备用的 else 块false。
swich 是一个条件语句,它计算表达式并将其与可能匹配的列表进行比较并执行相应的代码块,switch 只要匹配中了就会中止剩余的匹配项,switch 的 case 条件可以是多个值, 同一个 case 中的多值不能重复。
数组
具有相同 唯一类型 的一组以编号且长度固定的数据项序列,数据的长度是固定的。我们在声明一个数组时需要指定它的长度,一旦指定了长度,那么它的长度值是不可以改变的。
数组声明,一个数组的表示形式为 T[n]。n 表示数组中元素的数量,T 代表每个元素的类型,可以忽略声明数组的长度,并用 … 代替,让编译器为你自动计算长度。
数组是值类型,而不是引用类型,当数组赋值给一个新的变量时,该变量会得到一个原始数组的一个副本,如果对新变量进行更改,则不会影响原始数组。
o := [...]float64{67.4, 89.3, 21, 77}for i := 0; i < len(o); i++ {fmt.Println("%d th element of a is %.2f\n", i, o[i])}
使用 range 遍历数组 ,for 循环可用于遍历数组中的元素,range 返回索引和该索引处的值
for i, v := range a 利用的是 for 循环 range 方式
多维数组 [][] 数据类型 {}
j := [2][3]string{{"li", "wang", "oi"},{"cao", "sun", "hg"},}for _, v1 := range j {for _, v2 := range v1 {fmt.Println("%s", v2)}}
切片
是对数组一个连续片段的引用,是一个引用类型,是一个长度可变的数组,具有 T 类型元素的切片表示为[]T
使用语法 a[start:end] 创建一个从 a 数组索引 start 开始到 end - 1 结束的切片
a := [5]int{76, 77, 78, 79, 80}var b []int = a[1:4]fmt.Println(a)fmt.Println(b)
切片的修改:切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中。
a := [5]int{76, 77, 78, 79, 80}var b []int = a[1:4]fmt.Println(a)fmt.Println(b)for g := range b {b[g]++}fmt.Println(a)
切片的长度和容量:
切片的长度是切片中的元素数。切片的容量是从创建切片索引开始的底层数组中元素数。
funarry := [...]string{"1", "2", "3", "4", "5"}funlist := funarry[1:4]fmt.Println(len(funlist), cap(funlist))
切片可以重置其容量。任何超出这一点将导致程序运行时抛出错误。
funarry := [...]string{"1", "2", "3", "4", "5"}funlist := funarry[1:4]fmt.Println(len(funlist), cap(funlist))funlist = funlist[:cap(funlist)]fmt.Println(len(funlist), cap(funlist))
func make([]T,len,cap)[]T 通过传递类型,长度和容量来创建切片。容量是可选参数, 默认值为切片长度。make 函数创建一个数组,并返回引用该数组的切片。
r := make([]int, 5, 6)fmt.Println(r)
追加切片元素,使用 append 可以将新元素追加到切片上。append 函数的定义是 func append(s[]T,x … T)[]T。x … T 在函数定义中表示该函数接受参数 x 的个数是可变的。这些类型的函数被称为可变参函数。
car := []string{"福田", "宝马", "奔驰"}fmt.Println(car,len(car),cap(car))car = append(car, "雷克萨斯")fmt.Println(car,len(car),cap(car))
切片函数传递,切片包含长度、容量和指向数组第零元素的指针,切片原有空间不够时自动新建底层数组, 小于 1024, 新的容量直接翻倍增加
func subtactone(number []int) {for i := range number {number[i] -= 2}
}nos := []int{8, 7, 6}fmt.Println(nos)subtactone(nos)fmt.Println(nos)
多维切片,与数组类似,可以有多个维度
pos := [][]string{{"c++", "java"},{"python", "goland"},}for _, v1 := range pos {for _, v2 := range v1 {fmt.Println(v2)}}
map
一种无序的键值对, 它是数据结构 hash 表的一种实现方式
map[type of key]type of value{xxxxxxxx}
make(map[type of key]type of value)
look := map[string]int{"code": 1, "msg": 333}fmt.Println(look)cm := make(map[string]int)cm["name"] = 22fmt.Println(cm)
map的零值是nil
var say map[string]intsay["code"] = 10000
panic: assignment to entry in nil map
检索建的值 map[key]
citys := map[string]int{"北京": 110000,"广东": 430000,}city := "北京"postCode := citys[city]fmt.Println(city, postCode)
该映射将返回该元素类型的零值, 0 当我们尝试检索 Map中不存在的键的值时,不会出现运行时错误
citys := map[string]int{"北京": 110000,"广东": 430000,}city := "北京1"postCode := citys[city]fmt.Println(city, postCode)
检查键值是否存在 value, ok := map[key]
citys := map[string]int{"北京": 110000,"广东": 430000,}city := "北京"value, ok := citys[city]if ok == true {fmt.Println(value)postCode := citys[city]fmt.Println(city, postCode)}
遍历map中的所有元素 for循环的range形式用于迭代 Map的所有元素。
citys := map[string]int{"北京": 110000,"广东": 430000,}for key, value := range citys {fmt.Println(key, value)}
从map中删除元素 delete(map,key),函数没有返回值,删除 Map中不存在的键,则不会出现运行时错误。
citys := map[string]int{"北京": 110000,"广东": 430000,}delete(citys, "北京")for key, value := range citys {fmt.Println(key, value)}
与切片一样,maps 是引用类型。当一个 map 赋值给一个新的变量,它们都指向同一个内部数据结构。因此改变其中一个也会反映到另一个。
须指定 key, value 的类型,插入的纪录类型必须匹配。
key 具有唯一性,插入纪录的 key 不能重复。
KeyType 可以为基础数据类型(例如 bool, 数字类型,字符串), 不能为数组,切片,map,它的取值必须是能够使用 == 进行比较。
ValueType 可以为任意类型。
无序性。
线程不安全, 一个 goroutine 在对 map 进行写的时候,另外的 goroutine 不能进行读和写操作,Go 1.6 版本以后会抛出 runtime 错误信息。
字符串string
,通过将一组字符括在双引号中来创建字符串" "
%s 是打印字符串的格式说明符
%x 是十六进制的格式说明符
%b 是二进制的格式说明符
%d 是十进制的格式说明符
%o 是八进制的格式说明符
%t true或false
%c 格式说明符用于在printChars方法中打印字符串的字符
%T是用来输出数据类型的格式化占位符
%v输出所有的值信息
web := "https:www.gotribe.cn"fmt.Println(web)
字符串的单个字节
web := "https:www.gotribe.cn"fmt.Println(web)for i := 0; i < len(web); i++ {fmt.Println(web[i])}
在 UTF-8 编码中,一个码点可能会占一个以上的字节
rune 是Go 中的内置类型,它是 int32 的别名。Rune 表示 Go 中的 Unicode 代码点。代码点占用多少字节并不重要,它可以用一个符文来表示
web := "Señor"runes := []rune(web)for i := 0; i < len(runes); i++ {fmt.Printf("%c ", runes[i])}
字符串连接
使用+运算符
string1 := "1"string2 := "2"result := string1 + string2fmt.Println(result)
使用 fmt 包的Sprintf函数
string1 := "1"string2 := "2"string3 := "3"result := fmt.Sprintf("%x %x %s", string1, string2, string3)fmt.Println(result)
函数
是执行特定任务的代码块
func name(parameter) (result-list){//body
}
函数声明以func关键字开头,后跟name(函数名). 在括号中指定参数,后面为函数返回值result-list
指定参数的语法是,参数名称后跟类型。可以指定任意数量的参数,例如(parameter1 type, parameter2 type)。而{,}内的代码为函数的主体内容。
函数组成
函数名
参数列表(parameter)
返回值(result-list)
函数体(body)
参数和返回类型在函数中是可选的
单返回值函数
func plus(a, b int) (res int) {return a + b
}count := plus(1, 3)fmt.Println(count)
多返回值函数,一个函数可以返回多个值
func multi() (string, int) {return "网", 2
}
name, age = multi()fmt.Println(name, age)
命名返回值,从函数返回命名值。如果返回值被命名,则可以认为它在函数的第一行被声明为变量。
func nameValue() (name string, height int) {// name = "王"// height = 110return
}name, height := nameValue()fmt.Println(name, height)
参数可变函数
func sum(nums ...int) int {res := 0for _, v := range nums {res += v}return res
}
fmt.Println(sum(1))fmt.Println(sum(1, 2))fmt.Println(sum(1, 2, 3))
匿名函数
func(name string) {fmt.Println(name)}("web")
指针
是存储另一个变量的内存地址的变量。
指针的声明 指向类型 T 的指针用 *T 表示。
& 操作符用来获取一个变量的地址
package mainimport ("fmt"
)func main() {b := 255var a *int = &bfmt.Printf("%T \n", a)fmt.Println(a)
}
指针的空值,零值为nil
package mainimport ("fmt"
)func main() {b := 25var a *intif a == nil {fmt.Println(a)a = &bfmt.Println("is", b)}
}
使用新函数创建指针,new函数将一个类型作为参数并返回一个指针,该指针指向作为参数传递的类型的新分配的空值。
package mainimport ("fmt"
)func main() {c := new(int)fmt.Printf("%d %T %v", *c, c, c)*c = 50fmt.Println(" --- ", *c)
}
指针解引用,通过指针访问被指向的值。指针 a 的解引用表示为:*a
package mainimport ("fmt"
)func main() {a := 255b := &afmt.Println(b)fmt.Println(*b)*b++fmt.Println(a)
}
向函数传递指针参数
package mainimport ("fmt"
)func role() *int {i := 5return &i
}func main() {d := role()fmt.Println(*d)
}
不支持指针运算
package mainimport ("fmt"
)func role() *int {i := 5return &i
}func main() {d := role()d++fmt.Println(*d)
}
结构体
是由零个或多个任意类型的值聚合成的实体,它可以用于将数据分组为一个单元而不是将它们中的每一个作为单独的值。
声明一个结构体,关键字 type 和 struct 用来定义结构体
type StructName struct{FieldName type
}package mainimport ("fmt"
)type Order struct {name stringid int
}func main() {stu := Order{name: "王",id: 10000,}fmt.Println(stu)stu2 := Order{"中", 10001}fmt.Println(stu2)
}
创建匿名结构体,可以在不创建新数据类型的情况下声明结构。这些类型的结构称为匿名结构。
package mainimport ("fmt"
)func main() {temp := struct {name stringage int}{name: "王",age: 28,}fmt.Println(temp)
}
获取结构的各个字段,点.运算符用于访问结构的各个字段。
package mainimport ("fmt"
)func main() {temp := struct {name stringage int}{name: "王",age: 28,}fmt.Println(temp.name, temp.age)
}
结构体的指针,可以创建指向结构的指针。
package mainimport ("fmt"
)type Employee struct {name stringage int
}func main() {temp := &Employee{name: "wang",age: 80,}fmt.Println((*temp).name)fmt.Println((*temp).age)
}
匿名字段,创建具有仅包含类型而没有字段名称的字段的结构
package mainimport ("fmt"
)type Employee struct {stringint
}func main() {temp := &Employee{string: "wang",int: 80,}fmt.Println((*temp).string)fmt.Println((*temp).int)
}
嵌套结构体,一个结构可能包含一个字段,而该字段又是一个结构
package mainimport ("fmt"
)type Address struct {city stringstate string
}type Person struct {name stringage intaddress Address
}func main() {temp := Person{name: "wang",age: 90,address: Address{city: "gd",state: "Y",},}fmt.Println(temp.name)fmt.Println(temp.age)fmt.Println(temp.address.city)fmt.Println(temp.address.state)
}
当定义一个结构体变量,但是没有给它提供初始值,则对应的字段被赋予它们各自类型的零值。
方法
方法的声明
方法(method)的声明和函数很相似, 只不过它必须指定接收者:
func (t T) F() {}
注意:
接收者的类型只能为用关键字 type 定义的类型,例如自定义类型,结构体。
同一个接收者的方法名不能重复 (没有重载),如果是结构体,方法名还不能和字段名重复。
值作为接收者无法修改其值,如果有更改需求,需要使用指针类型。
package mainimport ("fmt"
)type Employee struct {name stringsalary intcurrency string
}func (e Employee) displaySalary() {fmt.Println(e.name, e.currency, e.salary)
}func main() {emp := Employee{name: "joyous",salary: 100,currency: "$",}emp.displaySalary()
}
指针接收器与值接收器型
package mainimport ("fmt"
)type Employee struct {name stringage int
}func (e *Employee) changeName(newName string) {e.name = newName
}func (e *Employee) changeAge(newAge int) {e.age = newAge
}func main() {e := Employee{name: "joyous",age: 20,}fmt.Println(e.name, e.age)e.changeName("joyous li")e.changeAge(25)fmt.Println(e.name, e.age)
}
changeName 方法有一个值接收器 (e Employee),而 changeAge 方法有一个指针接收器 (e *Employee)。在 changeName 中对 Employee 结构体的 name 字段所做的更改对调用者是不可见的,因此程序会在方法 e 之前和之后打印相同的 name。因为 changeAge 方法有一个指针接收器 (e *Employee),所以在方法调用 (&e) 之后对 age 字段所做的更改 (51) 将对调用者可见。
命名冲突
接收者定义的方法名不能重复,
package maintype T struct{}func (T) F() {}
func (T) F(a string) {}func main() {t := T{}t.F()
}
运行代码我们会得到 method redeclared: T.F 类似错误。
结构体方法名不能和字段重复
package maintype T struct{F string
}func (T) F() {}func main() {t := T{}t.F()
}
运行代码我们会得到 : type T has both field and method named F 类似错误。
同一个接收者的方法名不能重复 (没有重载);如果是结构体,方法名不能和字段重复。
接受者可以同时为值和指针
package maintype T struct{}func (T) F() {}func (*T) N() {}func main() {t := T{}t.F()t.N()t2 := &T{}t2.N()t2.F()
}
值作为接收者(T) 不会修改结构体值,而指针 *T 可以修改。
接口
接口类型是一种抽象类型,是方法的集合
package mainimport "fmt"type interface_name interface {method_name1() []rune
}type MyString stringfunc (ms MyString) method_name1() []rune {var vowels []runefor _, rune := range ms {if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {vowels = append(vowels, rune)}}return vowels
}func main() {name := MyString("sam anderson")var v interface_namev = namefmt.Printf("%c", v.method_name1())
}
空接口,一个没有方法的接口
package mainimport "fmt"func describe(i interface{}) {fmt.Printf("%T %v\n", i, i)
}func main() {s := "1111"describe(s)i := 555describe(i)str := struct {name string}{name: "2222",}describe(str)
}
类型断言,用于提取接口的基础值,i.(T)是用于获取具体类型为T的接口i的底层值的语法
package mainimport "fmt"func describe(i interface{}) {s := i.(int)fmt.Println(s)
}func main() {var f interface{} = 33describe(f)
}
同类型下可以正常转换使用
package mainimport "fmt"func describe(i interface{}) {s := i.(int)fmt.Println(s)
}func main() {var f interface{} = "311"describe(f)
}
不同类型下强转报错 panic: interface conversion: interface {} is string, not int
package mainimport "fmt"func describe(i interface{}) {s, ok := i.(int)fmt.Println(s, ok)
}func main() {var f interface{} = "311"describe(f)
}
变更一下语法 v, ok := i.(T),就可以实现程序不报错
如果 i 的具体类型是 T,那么 v 的值就是 i, ok 为 true。
如果 i 的具体类型不是 T,那么 ok 将是 false,v 将是类型 T 的零值
Goroutine
是与其他函数或方法同时运行的函数或方法,是轻量级线程
Goroutines 相对于线程的优势
与线程相比,Goroutines 非常便宜。它们的堆栈大小只有几 kb,堆栈可以根据应用程序的需要增长和缩小,而在线程的情况下,堆栈大小必须指定并固定。
Goroutine 被多路复用到更少数量的 OS 线程。一个包含数千个 Goroutine 的程序中可能只有一个线程。如果该线程中的任何 Goroutine 阻塞等待用户输入,则创建另一个 OS 线程并将剩余的
Goroutine 移动到新的 OS 线程。所有这些都由运行时处理,我们作为程序员从这些复杂的细节中抽象出来,并获得了一个干净的 API 来处理并发性。
Goroutines 使用通道进行通信。通道通过设计防止在使用 Goroutine 访问共享内存时发生竞争条件。通道可以被认为是Goroutine 进行通信的管道。
使用关键字为函数或方法调用添加前缀go
package mainimport ("fmt"
)func hello() {fmt.Println("hello world")
}func main() {go hello()fmt.Println("main function")
}
当一个新的 Goroutine 启动时,goroutine 调用立即返回。与函数不同,主程不等待 Goroutine 完成执行。在 Goroutine 调用之后,主程立即返回到下一行代码,并且 Goroutine 的任何返回值都将被忽略。
主 Goroutine 应该正在运行以供任何其他 Goroutines 运行。如果主 Goroutine 终止,则程序将终止,并且不会运行其他 Goroutine。
package mainimport ("fmt""time"
)func hello() {fmt.Println("hello world")
}func main() {go hello()time.Sleep(1 * time.Second)fmt.Println("main function")
}
用了time包的Sleep方法,该方法使正在执行的 go 例程休眠。在这种情况下,主 goroutine 会休眠 1 秒。现在调用go hello()有足够的时间在主 Goroutine 终止之前执行。该程序首先打印Hello world goroutine,等待 1 秒然后打印main function。
Channel
Channels可以被认为是 Goroutine 进行通信的管道。类似于水在管道中从一端流向另一端的方式,数据可以从一端发送并使用Channel从另一端接收。
chan T T是一个Channel类型
Channel的零值为nil。nil在Channels没有任何用处,因此必须使用make类似于maps和slices来定义Channel。
package mainimport ("fmt"
)func main() {var a chan intif a == nil {fmt.Println("======")a = make(chan int)fmt.Printf("%T", a)}
}
简写声明也是定义Channels的有效且简洁的方式。
a := make(chan int)
Channel发送和接收
箭头方向相对于Channel的指定数据是发送还是接收。
在第一行中,箭头指向外a,因此我们从Channels读取a并将值存储到变量data中。
在第二行中,箭头指向a,因此我们正在写入Channel。
package mainimport ("fmt"
)func hello(done chan bool) {fmt.Println("hello world")done <- false
}func main() {done := make(chan bool)go hello(done)<-donefmt.Println("main function")
}
死锁
如果一个 Goroutine 在一个 Channel 上发送数据,那么其他 Goroutine 应该正在接收数据。如果这没有发生,程序将在运行时陷入死锁。
func main() {done := make(chan bool)done <- false
}
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
F:/goland-code/src/test.go:14 +0x28
单向Channels
package mainimport ("fmt"
)func sendData(sendch chan<- int) {sendch <- 10
}func main() {sendch := make(chan<- int)go sendData(sendch)fmt.Println(<-sendch)
}
.\test.go:14:16: invalid operation: cannot receive from send-only channel sendch (variable of type chan<- int)
将双向 Channel 转换为仅发送或仅接收Channel,反之亦然。
package mainimport ("fmt"
)func sendData(sendch chan<- int) {sendch <- 10
}func main() {sendch := make(chan int)go sendData(sendch)fmt.Println(<-sendch)
}
创建了一个双向信道 chnl。在第 11 行它被作为参数传递给协程 sendData。第 5 行,sendData 函数通过形参 sendch chan<- int 将该信道转换成只写信道。因此在 sendData 中该信道为只写信道,而在主协程中该信道为双向信道。程序将打印:10。
关闭通道
发送者可以关闭通道以通知接收者该通道将不再发送数据。
接收者在从通道接收数据时可以使用一个额外的变量来检查通道是否已经关闭。
v, ok := <- ch
func main() {sendch := make(chan int)go sendData(sendch)v, ok := <-sendchfmt.Println(v, ok)
}
Buffered Channels
可以使用缓冲区创建通道。仅当缓冲区已满时,才会阻止发送到缓冲通道。同样,只有当缓冲区为空时,来自缓冲通道的接收才会被阻塞。
通过将附加容量参数传递给make指定缓冲区大小的函数来创建缓冲通道。
ch := make(chan type, capacity)
上述语法中的capacity应该大于 0,通道才能拥有缓冲区。默认情况下,无缓冲通道的容量为 0
package mainimport ("fmt"
)func main() {ch := make(chan string, 3)ch <- "naveen"ch <- "paul"fmt.Println(<-ch, <-ch)
}
select语句,用于从多个send/receive通道操作中进行选择,select语句会阻塞,直到其中一个发送/接收操作准备好。如果多个操作准备就绪,则随机选择其中一个。语法类似于switch,除了每个 case 语句都是一个通道操作
package mainimport ("fmt""time"
)func server1(ch chan string) {time.Sleep(6 * time.Second)ch <- "from server1"
}func server2(ch chan string) {time.Sleep(3 * time.Second)ch <- "from server2"
}func main() {output1 := make(chan string)output2 := make(chan string)go server1(output1)go server2(output2)select {case s1 := <-output1:fmt.Println(s1)case s2 := <-output2:fmt.Println(s2)}
}
Mutex互斥锁
当一个程序并发运行时,修改共享资源的部分代码不应该被多个Goroutines同时访问。修改共享资源的这段代码称为临界区。
使用互斥锁,限定临界区只能同时由一个线程持有,若是临界区此时被一个线程持有,那么其他线程想进入到这个临界区的时候,就会失败或者等待释放锁,持有此临界区的线程退出,其他线程才有机会获得这个临界区。
Mutex 用于提供一种锁定机制,以确保在任何时间点只有一个 Goroutine 正在运行代码的关键部分,以防止发生竞态条件。
Mutex上定义了两种方法,即Lock和Unlock。
mutex.Lock()
x = x + 1
mutex.Unlock()
x = x + 1在任何时间点都只会由一个 Goroutine 执行,从而防止竞争条件。
如果一个 Goroutine 已经持有锁,并且如果一个新的 Goroutine 正在尝试获取锁,则新的 Goroutine 将被阻塞,直到互斥锁被解锁。
具有竞争条件的程序
package mainimport ("fmt""sync"
)var x = 0func increment(wg *sync.WaitGroup) {x = x + 1wg.Done()
}func main() {var w sync.WaitGroupfor i := 0; i < 1000; i++ {w.Add(1)go increment(&w)}w.Wait()fmt.Println(x)
}
increment函数在第7行将x增加1。然后调用WaitGroup的Done()来通知其完成。
从第 10 行生成了 1000 个增量 Goroutine。这些 Goroutines 中的每一个都是并发运行的,并且当尝试增加 x 时会出现竞争条件。当多个 Goroutine 试图同时访问 x 的值时,在第8行尝试增加 x 时发生竞争条件。
使用互斥锁解决竞争条件,增加Mutexm的锁机制
package mainimport ("fmt""sync"
)var x = 0func increment(wg *sync.WaitGroup, m *sync.Mutex) {m.Lock()x = x + 1m.Unlock()wg.Done()
}func main() {var w sync.WaitGroupvar m sync.Mutexfor i := 0; i < 1000; i++ {w.Add(1)go increment(&w, &m)}w.Wait()fmt.Println(x)
}
使用channel解决竞争条件,创建了一个缓冲 channel 并指定容量为 1
package mainimport ("fmt""sync"
)var x = 0func increment(wg *sync.WaitGroup, ch chan bool) {ch <- truex = x + 1<-chwg.Done()
}func main() {var w sync.WaitGroupch := make(chan bool, 1)for i := 0; i < 1000; i++ {w.Add(1)go increment(&w, ch)}w.Wait()fmt.Println(x)
}
泛型
提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。在保证与Go 1完全兼容的情况下,为类型及函数声明增加可选的类型参数(Type Parameters)来扩展支持泛型函数和类型。类型参数使用接口类型(Interface Types)进行约束(Constraint),并允许在接口类型中内嵌附加元素来指定约束的类型集合。
没有泛型前的样例
package mainimport "fmt"func rInt(a int) int {return a
}func main() {a := 2222c := rInt(a)fmt.Println(c)
}
增加泛型后的样例
package mainimport "fmt"func rInt[T any](a T) T {return a
}func main() {c := rInt(111)fmt.Println(c)d := rInt("222")fmt.Println(d)
}
[T any] 即为类型参数,该函数支持任何类型的 slice,在调用该函数的时候,需要显式指定类型参数类型
约束,约束了这个泛型都具有哪些实际类型
[T any],T 表示类型的标识,any 表示 T 可以是任意类型。any 其实就是约束。
package mainimport "fmt"func add[T any](d, b T) T {return d + b
}func main() {c := add(111, 222)fmt.Println(c)
}
上述程序,定义了一个接收任一类型的两个值相加并返回结果。运行会报如下错
# command-line-arguments
.\test.go:6:9: invalid operation: operator + not defined on a (variable of type T constrained by any)
并非所有类型都支持加法操作。因此我们需要给出约束,指定可以进行加法操作的类型。
Go将interface的职责给扩展了,让接口不仅仅作为接口 — 解耦的,抽象化的结构体,还具有了约束,对于类型的约束作用。
package mainimport "fmt"type addType interface {int | float32 | float64
}func add[T addType](a, b T) T {return a + b
}func main() {c := add(111, 222)fmt.Println(c)
}
Defer
Defer 语句用于让函数或语句可以在当前函数执行完毕后执行
package mainimport "fmt"func finished() {fmt.Println("Finished")
}func largest(nums []int) {defer finished()fmt.Println("start")max := nums[0]fmt.Println(max)for _, v := range nums {if v < max {max = v}}fmt.Println(nums, max)
}func main() {nums := []int{78, 109, 2, 563, 300}largest(nums)
}
Defer方法,不仅限于函数。延迟方法调用也是完全合法的
package mainimport "fmt"type person struct {firstname stringlastname string
}func (p person) fullname() {fmt.Printf("%s %s", p.firstname, p.lastname)
}func main() {p := person{firstname: "li",lastname: "rui",}defer p.fullname()fmt.Printf("welcome ")
}
Defer堆栈,当一个函数有多个 defer 调用时,它们会被压入堆栈并以后进先出 (LIFO) 的顺序执行。
package mainimport "fmt"func main() {name := "hello"fmt.Printf("%s\n", string(name))for _, v := range name {defer fmt.Printf("%c", v)}
}
错误处理error
错误示例,尝试打开一个不存在的文件:
package mainimport ("fmt""os"
)func main() {f, err := os.Open("/test.txt")if err != nil {fmt.Println(err)return}fmt.Println(f.Name())
}
看看内置error类型是如何定义的。error是具有以下定义的接口类型:
type error interface { Error() string
}// http://golang.org/pkg/errors/error.go
// errors 构建 error 对象type errorString struct {s string
}func (e *errorString) Error() string {return e.s
}
error 有了一个签名为 Error() string 的方法。所有实现该接口的类型都可以当作一个错误类型。Error() 方法给出了错误的描述。
使用 New 函数创建自定义错误
package errorsfunc New(text string) error {return &errorString{text}
}type errorString struct {s string
}func (e *errorString) Error() string {return e.s
}package mainimport ("errors""fmt"
)func main() {err := errors.New("自定义错误")if err != nil {fmt.Println(err)}
}
errorString是具有单个字符串s字段的结构类型。第 14 行使用了 errorString 指针接收器来实现 error 接口的 Error() string 方法。第 5 行的 New() 函数有一个字符串参数,通过这个参数创建了 errorString 类型的变量,并返回了它的地址。于是它就创建并返回了一个新的错误。
Panic
有一些情况,程序在出现异常情况后无法继续执行。在这种情况下,我们使用panic提前终止程序。当一个函数遇到Panic时,它的执行被停止,任何延迟的函数都被执行,然后控制权返回给它的调用者。这个过程一直持续到当前goroutine的所有函数都返回,此时程序打印Panic消息,然后是堆栈跟踪,然后终止。
package mainimport ("fmt"
)func fullName(firstName *string, lastName *string) {if firstName == nil {panic("err first")}if lastName == nil {panic("err last")}fmt.Printf("%s %s", *firstName, *lastName)fmt.Println("")
}func main() {first := "Elon"fullName(&first, nil)fmt.Println("return")
}
recover
recover是一个内置函数,用于重新获得对Panic程序的控制权。
func recover() interface{}
仅当在延迟函数中调用时,Recover 才有用。在延迟函数中执行恢复调用会通过恢复正常执行来停止Panic序列,并检索传递给Panic函数调用的错误消息。如果在延迟函数之外调用恢复,它不会停止Panic序列。
package mainimport ("fmt"
)func recoverFullName() {if r := recover(); r != nil {fmt.Println("recover", r)}
}func fullName(firstName *string, lastName *string) {defer recoverFullName()if firstName == nil {panic("err first")}if lastName == nil {panic("err last")}fmt.Printf("%s %s", *firstName, *lastName)fmt.Println("")
}func main() {first := "Elon"fullName(&first, nil)fmt.Println("return")
}
当 fullName 发生 panic 时,会调用延迟函数 recoverName(),它使用了 recover() 来阻止 panic 后续事件。
最佳实践
在错误处理时,尽量不要使用 panic 和 recover。只有当程序不能继续运行的时候,才应该使用 panic 和 recover 机制。
当 if err != nil 时及时返回错误,从而避免过多的代码嵌套。
文件操作
读文件,最基本的文件操作之一是将整个文件读入内存。这是在ioutil包的ReadFile函数的帮助下完成的
package mainimport ("fmt""io/ioutil"
)func main() {data, err := ioutil.ReadFile("test.txt")if err != nil {fmt.Println(err)}fmt.Println(string(data))
}
写文件
写文件的步骤如下:
创建文件
将字符串写入文件
package mainimport ("fmt""os"
)func main() {f, err := os.Create("test.txt")if err != nil {fmt.Print(err)return}l, err := f.WriteString("hello world111111111")if err != nil {fmt.Println(err)f.Close()return}fmt.Println(l)err = f.Close()if err != nil {fmt.Println(err)return}
}
反射
reflect包在 Go 中实现了运行时反射。reflect 包有助于识别底层的具体类型和interface{}变量的值。这正是我们所需要的。该createQuery函数接受一个interface{}参数,并且需要根据参数的具体类型和值创建查询interface{}。这正是反射包的作用。
reflect.Type 和 reflect.Value
参数 interface{} 的具体类型由 reflect.Type 表示,而 reflect.Value 表示它的具体值。reflect.TypeOf() 和 reflect.ValueOf() 两个函数可以分别返回 reflect.Type 和 reflect.Value
package mainimport ("fmt""reflect"
)type order struct {id intname string
}func createQuery(q interface{}) {t := reflect.TypeOf(q)v := reflect.ValueOf(q)fmt.Println(t)fmt.Println(v)
}func main() {o := order{id: 1111,name: "244",}createQuery(o)
}
reflect.Kind
反射包中还有一种更重要的类型,称为Kind。
package mainimport ("fmt""reflect"
)type order struct {id intname string
}func createQuery(q interface{}) {t := reflect.TypeOf(q)v := t.Kind()fmt.Println(t)fmt.Println(v)
}func main() {o := order{id: 1111,name: "244",}createQuery(o)
}
NumField() 和 Field() 方法
NumField()方法返回结构中的字段数,Field(i int)方法返回字段 i 的 reflect.Value
package mainimport ("fmt""reflect"
)type order struct {id intname string
}func createQuery(q interface{}) {if reflect.ValueOf(q).Kind() == reflect.Struct {v := reflect.ValueOf(q)fmt.Println(v.NumField())for i := 0; i < v.NumField(); i++ {fmt.Printf("%d %T %v\n", i, v.Field(i), v.Field(i))}}}func main() {o := order{id: 1111,name: "244",}createQuery(o)
}
Int() 和 String() 方法
Int 和 String 可以帮助我们分别取出 reflect.Value 为 int64 和 string的值。
package mainimport ("fmt""reflect"
)func main() {a := 56x := reflect.ValueOf(a).Int()fmt.Printf("%T %v\n", x, x)b := "naveen"y := reflect.ValueOf(b).String()fmt.Printf("%T %v\n", y, y)}
完整的程序,sql生成器
package mainimport ("fmt""reflect"
)type order struct {id intname string
}type employee struct {name stringid intaddress stringsalary intcountry string
}func createQuery(q interface{}) {if reflect.ValueOf(q).Kind() == reflect.Struct {v := reflect.TypeOf(q).Name()query := fmt.Sprintf("insert into %s values (", v)t := reflect.ValueOf(q)for i := 0; i < t.NumField(); i++ {switch t.Field(i).Kind() {case reflect.Int:if i == 0 {query = fmt.Sprintf("%s%d", query, t.Field(i).Int())} else {query = fmt.Sprintf("%s,%d", query, t.Field(i).Int())}case reflect.String:if i == 0 {query = fmt.Sprintf("%s\"%s\"", query, t.Field(i).String())} else {query = fmt.Sprintf("%s,\"%s\"", query, t.Field(i).String())}default:fmt.Println("type")return}}query = fmt.Sprintf("%s)", query)fmt.Println(query)return}fmt.Println("type")
}func main() {o := order{id: 1111,name: "244",}createQuery(o)e := employee{name: "111",id: 111,address: "2222",salary: 22222,country: "sdasda",}createQuery(e)i := 90createQuery(i)
}
反射是 Go 中一个非常强大和先进的概念,应该谨慎使用。使用反射编写清晰且可维护的代码非常困难。应尽可能避免使用,仅在绝对必要时使用。
常用命令
go mod init initialize new module in current directory 在当前目录初始化mod
go mod tidy //拉取缺少的模块,移除不用的模块。
go mod download //下载依赖包
go mod vendor //将依赖复制到vendor下
go mod verify //校验依赖
go list -m -json all //依赖详情
go mod graph //打印模块依赖图
go mod why //解释为什么需要依赖