Go语言开发的开源项目,其中包括 Docker、Go-Ethereum、Thrraform 和 Kubernetes。
Go类似于Java,是编译型语言,自带编译器
1 Go基础
新版本的go 都是基于 go module 创建项目
Go.mod是Golang1.11版本新引入的官方包管理工具用于解决之前没有地方记录依赖包具体版本的问题,方便依赖包的管理。Modules和传统的GOPATH不同,不需要包含例如src,bin这样的子目录,一个源代码目录甚至是空目录都可以作为Modules,只要其中包含有go.mod文件。
Go.mod其实就是一个Modules,关于Modules的官方定义为:
Modules是相关Go包的集合,是源代码交换和版本控制的单元。go命令直接支持使用Modules,包括记录和解析对其他模块的依赖性。Modules替换旧的基于GOPATH的方法,来指定使用哪些源文件。
1.1 基本概念
【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数,下划线在代码意思是忽略这个变量
1.1.2 值类型:
bool
int(32 or 64), int8, int16, int32, int64
uint(32 or 64), uint8(byte), uint16, uint32, uint64
float32, float64
string,字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的
complex64, complex128
array -- 固定长度的数组
1.1.3 引用类型:(指针类型)
slice -- 序列数组(最常用)
map -- 映射
chan -- 管道
1.1.4 Go语言声明方式:
var(声明变量), const(声明常量), type(声明类型) ,func(声明函数)。
1.1.5 强制类型转换:
T(表达式),T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等.
1.1.6 格式化输出
1.2 命名规范
1.2.1 包名、文件名
package domain
package main
approve_service.go
1.2.2 变量名
type UserController struct
func isValidNumber(s string)
func Open(driverName, dataSourceName string) (*DB, error)
MAX_STOCK_COUNT
type PullRequestStatus int
const (
PULL_REQUEST_STATUS_CONFLICT PullRequestStatus = iota
PULL_REQUEST_STATUS_CHECKING
PULL_REQUEST_STATUS_MERGEABLE
)
var (
_defaultPort = 8080
_defaultUser = "user"
)
- 若变量、常量为 bool 类型,则名称应以 Has、Is、Can 或 Allow 开头:
var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool
- 如果模块的功能较为复杂、常量名称容易混淆的情况下,为了更好地区分枚举类型,可以使用完整的前缀:
type PullRequestStatus int
const ( PULL_REQUEST_STATUS_CONFLICT PullRequestStatus = iota PULL_REQUEST_STATUS_CHECKING PULL_REQUEST_STATUS_MERGEABLE
)
1.2.3 函数、方法名
- 函数、方法(结构体或者接口下属的函数称为方法)命名规则: 动词 + 名词。
- 若函数、方法为判断类型(返回值主要为 bool 类型),则名称应以 Has、Is、Can 或 Allow 等判断性动词开头:
func HasPrefix(name string, prefixes []string) bool { ... }
func IsEntry(name string, entries []string) bool { ... }
func CanManage(name string) bool { ... }
func AllowGitHook() bool { ... }
1.2.4 结构体、接口名
- 结构体命名规则:名词或名词短语。
- 接口命名规则:以 ”er” 作为后缀,例如:Reader、Writer。接口实现的方法则去掉 “er”,例如:Read、Write。
type Reader interface { Read(p []byte) (n int, err error)
}
// 多个函数接口
type WriteFlusher interface { Write([]byte) (int, error) Flush() error
}
1.3 数组 array
数组定义:var a [len]int,比如:var a [5]int
多维数组:var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}
//数组遍历test
var myString [5]int = [5]int{1, 2, 3, 4, 5}
for i := 0; i < len(myString); i++ {
fmt.Print(myString[i], ",")
}
fmt.Println()
for index, value := range myString {
fmt.Print(index, ":", value, ",")
}
fmt.Println()
// 二维数组遍历
var multiString [3][2]int = [...][2]int{{1, 2}, {3, 4}, {5, 6}}
for k1, v1 := range multiString {
for k2, v2 := range v1 {
fmt.Printf("(%d,%d)=%d,", k1, k2, v2)
}
fmt.Println()
}
1.4 切片 slice
切片跟数组的区别在于 Type 前的“ [] ”中是否有数字,为空,则代表切片,否则则代表数组。因为切片是长度可变的
var slice []type = make([]type, len, cap)
var slice []type = []type{1,2,3,4}
// 切片test
var slice = []int{1, 2, 3, 4}
var slice1 = make([]int, 4)
fmt.Printf("%v", slice) // [1 2 3 4]
fmt.Printf("%v", slice1)// [0 0 0 0]
原理:
假想每次传参都用数组,每次数组都要被复制一遍。消耗掉大量的内存。——函数传参用数组的指针。
函数传参问题:原数组的指针指向更改了,那么函数里面的指针指向都会跟着更改
切片优势:切片的指针和原来数组的指针是不同的
1.5 指针
指针地址、指针类型、指针取值
&arrayA //& 得到数组的内存地址,取地址
*[]int //* 获取内存地址对应值,取值
new函数得到的是一个类型的指针
a := new(int)//a := *int
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建
//用指针修改值
var num = 3
var ptr = &num
*ptr = 6
fmt.Println(num) // 6
1.6 集合 map
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
//maptest: map[keyType]valueType
var myMap map[string]int = make(map[string]int, 3)
myMap1 := make(map[string]int, 3)
myMap["liming"] = 12
myMap1["zhangming"] = 18
fmt.Println(myMap["liming"])
val, ok := myMap1["zhangming"]//判断集合是否包含某key
fmt.Println(val, ok)//18 true
//遍历map
for k, v := range myMap {
fmt.Print(k, "_", v)
}
1.7 结构体 struct
1.7.1 关键字type,实现自定义类的操作
// typetest
type MyInt int //自定义一种新类型叫MyInt
type NewInt = int //给int起一个小名叫NewInt
type person struct {
name string
age int
like map[string]string
have []int
}
1.7.2 正常操作
var myPerson person
myPerson.name = "Lily"
myPerson.age = 20
myPerson.have = []int{1, 2, 3, 4, 5}
myPerson.like = map[string]string{
"run": "yes",
"swim": "yes",
"bicycle": "yes",
}
fmt.Printf("%v", myPerson) // {Lily 20 map[bicycle:yes run:yes swim:yes] [1 2 3 4 5]}
1.7.3 利用指针实现结构体
//利用指针进行新建
var p = new(person) //new是新建了一个指针
p.name = "lucy"
fmt.Printf("%v", *p)//{lucy 0 map[] []}
//&
var person1 = &person{
name: "Make",
}
fmt.Printf("%v", person1)//&{Make 0 map[] []}
1.7.4 匿名结构体
var car struct {
Name string
Speed int
}
car.Speed = 120
car.Name = "BMW"
fmt.Printf("%v", car)
1.7.5 实现构造函数
type Student struct {
name string
age int
subject string
}
func newStudent(name string, age int, subject string) *Student {
return &Student{
name: name,
age: age,
subject: subject,
}
}
func main(){
student := newStudent("sommer", 17, "math")
fmt.Printf("%#v\n", student)//&main.Student{name:"sommer", age:17, subject:"math"}
}
1.7.6 方法——绑定
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
func (student Student) read(book string) {
fmt.Println(student.name, "读", book)
}
指针方法原值,拷贝方法不改变原值
注意:
当传递的是值拷贝类型时,不能传递指针*person和值&person
当传递的是指针类的时候,指针类型和值类型的变量均可相互调用
func main(){
student.changSubject("history")
fmt.Printf("%#v\n", student)//&main.Student{name:"sommer", age:17, subject:"history"}
student.changSubject2("history")
fmt.Printf("%#v\n", student)//&main.Student{name:"sommer", age:17, subject:"math"}
}
func (student *Student) changSubject(newSubject string) {
student.subject = newSubject
}
func (student Student) changSubject2(newSubject string) {
student.subject = newSubject
}
1.7.7 嵌套匿名结构体——实现继承
所有的内置类型和自定义类型都是可以作为匿名字段去使用
func main(){
myStudent := Student{
name: "Jack",
age: 18,
subject: "English",
Person: Person{//匿名结构体直接嵌套,如下如果是*Person,那么这里应该是&Person
hight: 180,
},
}
fmt.Printf("%#v\n", myStudent)//main.Student{name:"Jack", age:18, subject:"English", Person:main.Person{hight:180}}
}
type Student struct {
name string
age int
subject string
Person// 对应的如果这里是*Person
}
type Person struct {
hight int
}
2 流程控制
2.1 if
if a < 20 {
fmt.Printf("a 小于 20\n" )
} else {
fmt.Printf("a 不小于 20\n" )
}
2.2 switch
//Go里面switch默认相当于每个case最后带有break,匹配成功自动跳出整个switch,但是可以使用fallthrough强制执行后面的case代码
//如果switch没有表达式,它会匹配true
switch i := x.(type) { // 带初始化语句
case nil:
fmt.Printf(" x 的类型 :%T\r\n", i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型")
default:
fmt.Printf("未知型")
}
2.3 select
2.4 for——没有while
基本三种语法
s := "abc"
for i, n := 0, len(s); i < n; i++ { // 常见的 for 循环,支持初始化语句。
println(s[i])
}
n := len(s)
for n > 0 { // 替代 while (n > 0) {}
println(s[n]) // 替代 for (; n > 0;) {}
n--
}
for { // 替代 while (true) {}
println(s) // 替代 for (;;) {}
}
2.5 range
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。
for key, value := range oldMap {
newMap[key] = value
}
1st value | 2nd value | ||
---|---|---|---|
string | index | s[index] | unicode, rune |
array/slice | index | s[index] | |
map | key | m[key] | |
channel | element |
3 函数
3.1 基本语法
func test(x, y int, s string) (int, string) {
// 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
n := x + y
return n, fmt.Sprintf(s, n)
}
// 利用别名操作
// 定义函数类型。
type FormatFunc func(s string, x, y int) string
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
3.2 参数
在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。map、slice、chan、指针、interface默认以引用的方式传递。
可变参数在参数后加上“…”即可,
其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.
func myfunc(args ...int) { //0个或多个参数
}
func add(a int, args…int) int { //1个或多个参数
}
func add(a int, b int, args…int) int { //2个或多个参数
}
用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。
func myfunc(args ...interface{}) {
}
(slice...)
package main
import (
"fmt"
)
func test(s string, n ...int) string {
var x int
for _, i := range n {
x += i
}
return fmt.Sprintf(s, x)
}
func main() {
s := []int{1, 2, 3}
res := test("sum: %d", s...) // slice... 展开slice
println(res)
}
3.3 闭包
3.3.1 函数变量
func square(x int) {
println(x * x)
}
square(1)s := squares(1)square
3.3.2 闭包
闭包 = 函数 + 相关引用环境,组合而成的实体,闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改
str := "hello world"
foo := func() {
str = "hello dude" // 匿名函数中并没有定义 str,str 的定义在匿名函数之前,str 就被引用到了匿名函数中形成了闭包
}
foo()//执行闭包,此时 str 发生修改,变为 hello dude。
把整个匿名函数当做函数变量,用另一个函数进行整合,就形成了常见的模式
// 1
func() int {//无输入参数,返回int
var x int
func() {
x++
}
return x
}
// 2 整合
func() int {
var x int
return func() int {
x++
return x
}
}
// 3 作为另一个函数的返回值
func incr() func() int {
var x int
return func() int {
x++
return x
}
}
i := incr() //通过把这个函数变量赋值给 i,i 就成为了一个闭包。i 保存着对 x 的引用
println(i()) // 1
println(i()) // 2
println(i()) // 3
3.4 延迟调用
3.4.1 defer用途:
1. 关闭文件句柄
2. 锁资源释放,defer 是先进后出
3. 数据库连接释放
3.4.2 注意defer使用关闭资源
type Member struct {
name string
}
func (member *Member) Close() {
fmt.Println(member.name, "closed")
}
func main(){
arr := []Member{{"a"}, {"b"}, {"c"}}
for _, m := range arr {
myMember := m //这里需要多一步赋值,不然无法正常关闭资源
defer myMember.Close()
}
}
// c closed
// b closed
// a closed
3.4.3 defer和return的关系
A. 无名返回值的情况
package main
import (
"fmt"
)
func main() {
fmt.Println("return:", a()) // 打印结果为 return: 0
}
func a() int {
var i int
defer func() {
i++
fmt.Println("defer2:", i) // 打印结果为 defer: 2
}()
defer func() {
i++
fmt.Println("defer1:", i) // 打印结果为 defer: 1
}()
return i
}
B. 有名返回值的情况
package main
import (
"fmt"
)
func main() {
fmt.Println("return:", b()) // 打印结果为 return: 2
}
func b() (i int) {
defer func() {
i++
fmt.Println("defer2:", i) // 打印结果为 defer: 2
}()
defer func() {
i++
fmt.Println("defer1:", i) // 打印结果为 defer: 1
}()
return i // 或者直接 return 效果相同
}
原因
return会将返回值先保存起来,对于无名返回值来说, 保存在一个临时对象中,defer是看不到这个临时对象的;
而对于有名返回值来说,就保存在已命名的变量中。整个过程中这个变量的内存地址并没有变化,所以此时defer会修改实际返回的结果
3.5 错误处理
defer、panic 和 recover的配合
panicpanicdeferrecoverpanicdefer
func main(){
testError()
}
func getAeril(radius float32) (area float32) {
if radius < 0 {
panic("注意!半径不能为负数")
} else {
return radius * radius * 3.14
}
}
func testError() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
getAeril(-3)//出错调用pinic,得到err,跳转到defer执行,运行到recover()继续程序的执行
fmt.Println("错误之后的内容执行") //没有执行
}
4 接口
接口是一类方法的集合,命名以er结尾。
实现接口非显式(implement interface),一个对象只要全部实现了接口中的方法,那么就实现了这个接口。
只有当有两个或两个以上的具体类型必须以相同的方式进行处理时才需要定义接口。不要为了接口而写接口,那样只会增加不必要的抽象,导致不必要的运行时损耗
4.1 接口基本语法
一个结构体可以实现多个接口,一个接口可以被多个结构体实现
//接口
type Animaler interface {
say(voice string)
jump(hight int)
}
// Tiger实现接口的所有方法实现接口
type Tiger struct {
name string
}
func (tiger Tiger) say(voice string) {
fmt.Println("我是森林之王,", voice, "分贝")
}
func (tiger Tiger) jump(hight int) {
fmt.Println("森林之王跳高,", hight, "米")
}
//Lion实现接口的所有方法实现接口
type Lion struct {
name string
}
func (lion Lion) say(voice string) {
fmt.Println("我是狮子,", voice, "分贝")
}
func (lion Lion) jump(hight int) {
fmt.Println("狮子跳高,", hight, "米")
}
接口的作用:**接口类型变量能够存储所有实现了该接口的实例。**a实例可以直接赋值给接口实现类,b实例也可以直接赋值给接口实现类
func main(){
var x Animaler //接口类型变量能够存储所有实现了该接口的实例
x = Tiger{name: "tiger"}
x.say("tiger")
x.jump(100)
x = Lion{name: "lion"}
x.say("lion")
x.jump(1000)
}
4.2 值接收和指针方法接收者的区别
type Mover interface {
move()
}
func (d dog) move() {
fmt.Println("狗会动") //值接受型即可接收值也可接收指针
}
var x Mover
x = dog{} // x是dog值类型
x = &dog{} // x可以接收dog指针类型
func (d *dog) move() { //指针接收型只能接收指针
fmt.Println("狗会动")
}
4.3 空接口
空接口是指没有定义任何方法的接口。因此任何类型都实现了空接口。
4.3.1 空接口作为函数的参数
使用空接口实现可以接收任意类型的函数参数。
// 空接口作为函数参数
func show(a interface{}) {
}
4.3.2 空接口作为map的值
使用空接口实现可以保存任意值的字典。
// 空接口作为map值
var studentInfo = make(map[string]interface{})
studentInfo["name"] = "李白"
studentInfo["age"] = 18
studentInfo["married"] = false
fmt.Println(studentInfo)
4.3.3 类型断言
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T)
其中:
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。
func justifyType(x interface{}) {
switch v := x.(type) {
case string:
fmt.Printf("x is a string,value is %v\n", v)
case int:
fmt.Printf("x is a int is %v\n", v)
case bool:
fmt.Printf("x is a bool is %v\n", v)
default:
fmt.Println("unsupport type!")
}
}
5 并发编程
5.1 goroutine
goroutine 只是由官方实现的超级"线程池"。
4~5KB
java中:需要自己维护线程池和调度线程、上下文切换
goroutine内置调度和上下文切换的机制,不需要自己写进程、线程、协程,只需要把任务包装成一个函数,开启一个goroutine去执行这个函数
在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
var wg sync.WaitGroup
func main() {
// 并发编程test
for i := 0; i < 10; i++ {//创建10个goroutine运行
wg.Add(1)
go hello(i)
}
wg.Wait()//等所有线程都执行完毕再结束
}
func hello(index int) {
defer wg.Done()
fmt.Println("hello! goroutine!", index)
}
在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束。
5.2 channel
并发通信,go通信共享内存而不是通过共享内存而实现通信。如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
5.2.1 channel类型
channel是一种类型,一种引用类型。声明通道类型的格式如下:
var 变量 chan 元素类型
举几个例子:
var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道
channel是引用类型,需要make创建内存,没有创建时为nil,创建之后直接打印是地址
<-
var ch1 chan int //ch1:<nil>
ch2 := make(chan int, 2) //make(chan 元素类型, [缓冲大小])
ch2 <- 10 // ch2:0xc0000120e0
ch2 <- 11
x := <-ch2
fmt.Println(x) //10
5.2.2 channel收发
单通道无缓存,无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作;接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。
接收数据
func recv(c chan int) {//接收数据
res := <-c
fmt.Println("receive", res)
}
发送数据
func main() {
ch := make(chan int)
go recv(ch)
ch <- 10 // 开启通道,发送数据
fmt.Println("champion")
}
有缓存
我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:
func main() {
ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
ch <- 10
fmt.Println("发送成功")
}
5.2.4 关闭通道
内置的close()函数关闭channel(如果你的管道不往里存值或者取值的时候一定记得关闭管道)
func main() {
c := make(chan int)
go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()
}
当通道被关闭时,往该通道发送值会引发panic,从该通道里接收的值一直都是类型零值。那如何判断一个通道是否被关闭了呢?
有两种方式在接收值的时候判断通道是否被关闭,我们通常使用的是for range的方式。
i, ok := <-ch1 // 通道关闭后再取值ok=false
if !ok {
break
}
for i := range ch2 { // 通道关闭后会退出for range循环
fmt.Println(i)
}
5.3 select
Go内置了select关键字,可以同时响应多个通道的操作。
select {
case <-chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程
}
5.4 加锁
Go语言中使用sync包的Mutex类型来实现互斥锁。
var lock sync.Mutex
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
读写锁示例:
var (
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
rwlock.Lock() // 加写锁
x = x + 1
rwlock.Unlock() // 解写锁
}
func read() {
rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
rwlock.RUnlock() // 解读锁
}
5.5 并发任务的同步
Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 注意sync.WaitGroup是一个结构体,传递的时候要传递指针。
sync.WaitGroup有以下几个方法:
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。
5.6 其他
5.6.1 解决执行一次场景
只执行一次场景的解决方案–sync.Once。sync.Once只有一个Do方法,其签名如下:
func (o *Once) Do(f func()) {}
注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。
5.6.2 解决map的线程安全
Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
5.6.3 原子操作
Go语言中原子操作由内置的标准库sync/atomic提供