**GOROOT:**Go语言所在的目录,用于全局执行Go相关的命令
**GOPATH:**工作目录,工程代码存放的位置,此目录下,一个文件夹就是一个工程
**GOPROXY:**代理,由于Go需要使用,需要配置代理
地址:https://goproxy.io/zh/ 查看文档
go env 检查环境变量是否配置成功
创建一个 hello 的文件夹
在 hello 文件夹中创建一个 main.go 文件(文本编辑器中编写)
// package 定义包名 main
package main
// import 引用库 fmt
import "fmt"
// func 定义函数 main 函数名
func main(){
// fmt 包名 . 调用 Print 函数,并且输出定义的字符串
fmt.Print("Hello GoLang From Levin.com")
}
在 cmd 中输入指令 go env 运行
// 编译
go build 文件名
// 运行
.\ 文件名
// 快速运行
go run 文件名
package(创建包)
Go语言以“包”为管理单位,每个Go的源文件都必须声明它所属的包,其声明如下:
// package--关键字,name--包名
package name
Go包的注意点:
main
包是Go程序的入口包,每个程序有且只能有一个 main
包,如果一个程序中并不存在 main
包,编译时会出错import(导入包)
两种方式:
// 第一种--单个包
import "name"
// 第二种 多个包 注意:包后无需加逗号
import {
"name_01"
"name_02"
}
main函数(入口函数)
main
函数只能存在于 main
包中,每个 main
包有且只能有一个 main
函数
Go语言是一门编译型的静态语言
两种编译方式:
go build
,该命令可以将Go语言的代码编译成一个可执行文件(二进制) ,使用该命令,我们需要手动开启程序go run
,该命令对代码进行编译并执行可执行文件go build [fileName_01] [fileName_02]
go mod init 文件夹
go mod 和 go sum
注意跨包使用函数需要大写!!!
上官网下载并安装即可
gofmt
程序(go fmt
)将Go程序按照标准风格缩进、对齐
标准包中的所有Go代码都已经用 gofmt
格式化过
获取器
Go语言对 getter
和 setter
提供自动支持,大写字母可以跨包使用!
接口名
按照约定,只包含一个方法的接口一般在该方法后加上 “er” 来命名
Go语言是静态类型语言,变量是有明确类型的,编译器也会检查变量类型的正确性。
从计算机系统的角度来讲,变量就是一段或多段内存,用于存储数据
var 变量名 变量类型
变量声明以关键字var开头,变量类型后置,行尾无须分号
// 声明一个名为 age 的变量,类型为int
var age int
变量命名规则准寻驼峰命名法,即首个字母小写
计算机中数据存储的最小单元为bit(位),0或者1
byte:计算机中数据的基本单元,1字节 = 8bit,数据在计算机中存储或计算,至少为1个字节
有符号和无符号的区别:int8 范围为:-128~127,uint8 范围为:0-255
当一个变量被声明后,系统自动赋予该类型的零值
所有的内存在Go中都是有经过初始化的
// 设置游戏中角色的初始等级为1
var level = 1
Go语言中,在编译时会自动推到其类型
//var level float32 = 1.0
func main(){
fmt.Printf("%T", level)
}
/*
输出结果为:
float32
*/
var(
a int
b string
c []float32
)
package main
import "fmt"
var(
a int
b float32
c string
)
func main(){
// %d 整数占位符, %s 字符串占位符号, %f 浮点数占位符(默认6为精确度)
fmt.Printf("a=%d,b=%f,c=%s", a, b, c)
}
使用简短格式有以下限制:
简短变量声明被广泛使用于大部分的局部变量的声明和初始化,var形式的声明语句往往用于需要显示指定变量类型的地方
可以通过 var
或者短变量来初始化赋值变量
在多个短变量声明和赋值中,至少有一个新声明的变量出现在左值中,即便其他变量名可能是重复声明的,编译器也不会报错
标准格式:
// var 变量名 类型 = 表达式
var level int = 10
编译器自动推导类型的格式:
var level = 10 // 10 -- 右值
短变量声明初始化:
level := 10
注意:由于短变量声明初始化中使用 :=
而不是 =
,因此此种初始化的左值变量必须是没有定义过的变量,若声明过,则编译错误
var a int = 10
var b int = 20
// 第一种方式
a = a^b
b = b^a
a = a^b
// 第二种方式 -- 多重赋值
a, b = b, a
根据变量定义位置的不同,可以分为以下三个类型:
匿名变量
没有名称的变量、类型或方法,统称为匿名变量
匿名变量的特点:一个下划线 “_”,下划线本身就是一个特殊的标识符,陈伟空白标识符
func GetValue() (int, int){
return 100, 200
}
func main(){
a, _ := GetValue()
_, b := GetValue()
fmt.Println(a, b)
}
匿名变量不占用内存空间,不会分配内存,匿名变量与匿名变量之间也不会因为多次声明而无法使用。
局部变量
局部变量不是一直存在的,它只在定义它的函数被调用后存在,函数调用结束后这个局部变量就会被销毁。
全局变量
全局变量声明必须以 var
关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。
形式参数
形式参数会作为函数的局部变量来使用。
有符号整型:
int8、int16、int32 和 int64
无符号整型:
uint8、uint16、uint32 和 uint64
浮点数取值范围的极限值可以在 math 包中找到:
math.MaxFloat32
表示 float32 能取到的最大数值,大约是 3.4e38;math.MaxFloat64
表示 float64 能取到的最大数值,大约是 1.8e308;浮点数在声明的时候可以只写整数部分或者小数部分,如:
var a = .1234 //0.1234
var b = 1. // 1
Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。
复数的声明如下:
var value complex128 = complex(a, b) // a和b为float64类型数值
// 获取实部
real(value)
// 获取虚部
imag(value)
布尔类型的值:true
和 false
非0即 true
一个字符串是一个不可改变的字节序列,字符串可以包含任意的数据。
字符串是 UTF-8
字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。
字符串是一种值类型,且值不可变,即创建某个文本后将无法再次修改这个文本的内容,可以说,字符串是字节的定长数组。
一般的比较运算符(==、!=、<、<=、>=、>)是通过在内存中按字节比较来实现字符串比较的,因此比较的结果是字符串自然编码的顺序。字符串所占的字节长度可以通过函数 len()
来获取,例如 len(str)
。
字符串的内容(纯字节)可以通过标准索引法来获取,在方括号[ ]
内写入索引,索引从 0 开始计数:
需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效(因为 ASCII 码的字符只占用一个字节)。
字符串的定义
使用双引号 ""
来定义字符串
package main
import "fmt"
func main(){
var str = "Hello World!"
fmt.Println(str)
}
字符串的拼接
两个字符串拼接可以通过 +
拼接在一起,如:
var str_01 = "Hello "
var str_02 = "World!"
str := str_01+str_02
也可以采用 +=
的形式:
str := ""
str00 := "yeah!"
str += str00
字符串的实现
Go中的字符串内部实现是使用 UTF-8
编码,通过 rune
类型(rune
---- uint8
),对每个 UTF-8
字符进行访问
多行字符串
字符串字面量:双引号书写字符串,不能跨行
多行字符串需要采用 \
,如下:
str := `The first line
The Second Line
The Last Line!
\n
`
在多行字符串的方式下,所有的转义字符均无效,字符串会正常输出
字符串是由字符组成的,Go语言中的字符有两种:
uint8
(byte
) 类型,表示一个 ASCII码表中的字符rune
类型,代表一个 UTF-8
字符,处理中文等复合字符时,需要 rune
类型,即 uint32
类型除了 UTF-8
编码外,Go也同样支持 Unicode
编码,在内存中用 int
表示,在文档中,一般以 U+hhhh
的格式表示,h
为一个十六进制数
注意:%c
输出字符,%v
和 %d
输出该字符对应的整数,%U
输出 U+hhhh
Unicode 包中内置了与字符相关的函数,可以查询相关文档使用
UTF-8
为编码方式
Unicode
和 ASCII 都是字符集
在 UTF-8
的编码方式下,拉丁文的字符编码一般每个字符占用一个字节,中文字符占用3个字节
Go语言不存在隐式的类型转换
类型转换声明:
// value_01与value_02不同类型
// 类型1的值 = 类型1(类型2的值)
value_01 = type(value_02)
// 实例
value_t1 := 5.0
value_t2 := int(a)!
注意:类型转换只能在正确的情况下转换成功,精度高的数值转换成精度低的数值,精度会损失
Go语言提供了控制数据结构指针的能力,但不能进行指针运算,Go语言允许你控制特定集合的数据结构、分配的数量以及内存访问模式
指针的两个核心概念
指针地址和指针类型
一个指针变量可以指向任何一个值的内存地址,它所指向的值的内存地址在 32 和 64 位机器上分别占用 4 或 8 个字节,占用字节的大小与所指向的值的大小无关。当一个指针被定义后没有分配到任何变量时,它的默认值为 nil
。
v := 10
ptr := &v
此处 ptr
的类型为 *int
,称为 int
的指针类型
地址的打印通过 %p
表示
var value = 10
ptr := &value
fmt.Printf("%p %p", &value, ptr)
获取指针指向的值
通过 *
来获取指针的值
var str = "Hello World!"
ptr := &str
fmt.Prinln("%s", *ptr)
指针修改值
var a int = 10
var b int = 20
temp := &a
*a = *b
*b = *temp
new()函数可以创建指针
strPtr := new(string)
常量的定义使用关键字 const
定义,编译时即被创建
常量的定义:
// const name [type] = vlaue
const name = "String"
iota常量生成器
常量声明可以使用 iota
常量生成器初始化,它用于生成一组以相似规则初始化的常量,但不用每行都写一遍初始化表达式。
在一个 const
声明语句中,在第一个声明常量所在行,iota
将会被置为 0,然后在每一个有常量声明所在行加一。
无类型常量
编译器为这些没有明确基础类型的数字常量提供比基础类型更高精度的算术运算,可以认为至少有 256bit
的运算精度。
有六种未明确类型的常量类型,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。
// 无类型常量
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi
fmt.Printf("x = %v, y = %v, z = %v\n", x, y, z)
const Pi64 float64 = math.Pi
var x2 float32 = float32(Pi64)
var y2 float64 = Pi64
var z2 complex128 = complex128(Pi64)
fmt.Printf("x2 = %v, y2 = %v, z2 = %v\n", x2, y2, z2)
Go1.9版本前:
type abc uint8
Go1.9版本后:
type abc = uint8
类型定义与类型别名
类型定义:
type MyTime time.Duration
类型别名:
type MyTime = time.Duration
非本地类型不能定义方法,即非本包定义的数据类型
Go中的关键字一共有25个
关键字不能作为标识符使用
标识符的命名需要遵守以下规则:
_
组成;break
,if
等等。命名标识符时还需要注意以下几点:
stuName
、getVal
;当然Go语言中的变量、函数、常量名称的首字母也可以大写,如果首字母大写,则表示它可以被其它的包访问(类似于 Java 中的 public
);如果首字母小写,则表示它只能在本包中使用 (类似于 Java 中 private
)。
优先级 | 分类 | 运算符 | 结合性 |
---|---|---|---|
1 | 逗号运算符 | , | 从左到右 |
2 | 赋值运算符 | =、+=、-=、*=、/=、 %=、 >=、 <<=、&=、^=、|= | 从右到左 |
3 | 逻辑或 | || | 从左到右 |
4 | 逻辑与 | && | 从左到右 |
5 | 按位或 | | | 从左到右 |
6 | 按位异或 | ^ | 从左到右 |
7 | 按位与 | & | 从左到右 |
8 | 相等/不等 | ==、!= | 从左到右 |
9 | 关系运算符 | <、<=、>、>= | 从左到右 |
10 | 位移运算符 | <<、>> | 从左到右 |
11 | 加法/减法 | +、- | 从左到右 |
12 | 乘法/除法/取余 | *(乘号)、/、% | 从左到右 |
13 | 单目运算符 | !、*(指针)、& 、++、–、+(正号)、-(负号) | 从右到左 |
14 | 后缀运算符 | ( )、[ ]、-> | 从左到右 |
介绍
数组属于值类型
数组是一个由固定长度的特定类型元素组成的序列,一个数组中的元素可能为0或多个
声明
// var 数组变量名 [元素数量]Type
var data [13]int
默认情况下,数组的每个元素都会被初始化为元素类型对应的零值,对于数字类型来说就是 0,同时也可以使用数组字面值语法,用一组值来初始化数组:
var data [2]int = [2]int{1, 2}
var data2 [3]int = 3[int]{1, 2}
在数组的定义中,如果在数组长度的位置出现“…”省略号,则表示数组的长度是根据初始化值的个数来计算,如下:
data := [...]int{1, 2, 3, 4}
注意:数组的长度需要在编译阶段请确定,长度必须是常量表达式
数组的比较
只有两个数组的类型相同,才可以进行比较[类型指的是元素类型及数组长度]
可以通过 ==
和 !=
判断两个数组是否相对
多维数组
声明如下:
// var 数组名称 [size_01][size_02]...[size_n]Type
var data [4][2]int
data = [4][2]int{{1, 2}, {2, 3}, {3, 4}, {4, 5}}
介绍
切片是对数组的一个连续片段的引用,属于引用类型
切片的内部结构包含:地址、大小、容量
切片的获取
// slice [start:end]
a := [4]int{1, 2, 3, 4}
a[0:2]
生成切片的特性
slice[len(slice)]
获取;切片的声明
// var name []Type
var str []string
切片的创建----make
通过 make()
可以动态地创建一个切片
// make([]Type, size, cap)
s := make([]int, 10, 12)
切片添加元素----append
可以通过 append()
为切片动态添加元素,如下:
var array []int
array = append(array, 1)
array = append(array, 1, 2, 3, 4)
array = append(array, []int{1, 2, 3}...)
切片复制
内置函数 copy()
可以实现切片的复制
copy()
的使用方法如下:
copy(dest [] Type, src []Type)int
注意:切片复制不是引用,改变副本值不会改变原有切片的值
切片的删除
切片的删除可以通过移动数据指针或者覆盖数据来实现
即通过改变的起始引用位置来实现
注意:切片的删除效率 较低,当业务需要大量的删除操作,一般不使用切片
range关键字
通过 range
来配合 for
实现迭代,直接上例子:
s := []int{1, 2, 3}
for i, v in range s{
fmt.Println(v)
}
for _, v in range s{
fmt.Println(v)
}
多维切片
声明:
// var slice [][]...[]Type
slice := [][]int{{10}, {10, 20}}
slice[0] = append(slice[0], 20)
map
是引用类型
map
是一种元素对的无序集合,一个元素由一个 key
和一个 value
组成
声明和创建
声明,无需指定长度,map
类型是可以自动增长
var name map[keyType]valueType
// 键值的类型之间允许有空格
var m1 map[string] string
m1 = map[string]string{"Name": "Levin", "Age": "21"}
创建的方法
m := map[string] int{}
m := make(map[string] int)
容量
map
可以动态的伸缩,不存在固定长度或者最大限制,但可以设置 map
的初始容量 capacity
// make(map[keyType] valueType, capacity)
m := make(map[string] string, 10)
一对多情况
用切片作为 map
的值
当一个应用需要一个 key
对应多个 value
时,可以将 value
设置为数组类型
m1 := make(map[string] []int)
m2 := make(map[string] *[]int)
map
的遍历
直接上例子
m := map[string] int{"First": 1, "Second": 2, "Third": 3}
for k, v := range m{
fmt.Println(k, v)
}
map
的删除清空
内置函数 delete()
可以用于删除容器的元素
// delete(map, key)
m := map[string] int{"abc": 123, "i": 1}
delete(m, "i")
清空 map
的唯一方法就是 make
一个新的 map
m := map[string] int{"abc": 123, "i": 1}
m = make(map[string] int)
sync.Map
在并发情况下,map
在只读条件下是线程安全的,在读写状态下是不安全的
sync.Map
有以下特性:
sync.Map
不能使用 map
的方式进行取值和设置等操作,而是使用 sync.Map
的方法进行调用,Store
表示存储,Load
表示获取,Delete
表示删除Range
配合一个回调函数进行遍历操作,通过回调函数返回内部遍历出来的值,Range
参数中回调函数的返回值在需要继续迭代遍历时,返回 true
,终止迭代遍历时,返回 false
上例子:
var sMap sync.Map
// 保存 Store
sMap.Store("Green", 1)
sMap.Store("Red", 2)
sMap.Store("Yellow", 3)
// 获取 Load
sMap.Load("Green")
// 删除 Delete
sMap.Delete("Green")
// 遍历 匿名函数--回调函数,参数类型为 interface{}
sMap.Range(func(k, v interface{})) bool{
fmt.Prinln(k, v)
return true
})
列表是一种非连续的存储容器,由多个节点组成,节点通过一些变量记录彼此之间的关系,列表有多种实现方法,如单链表、双链表等。
初始化列表
list
的初始化有两种方法:
New()
函数var
关键字// 第一种
name := list.New()
// 第二种
var name list.List
插入元素
双链表支持队列前后方插入元素,方法为:PushFront()
和 PushBack()
这两个方法都会返回 list.Element
结构
l := list.New()
l.PushFront("Hello ")
l.PushBack("World!")
方 法 | 功 能 |
---|---|
InsertAfter(v interface {}, mark * Element) * Element | 在 mark 点之后插入元素,mark 点由其他插入函数提供 |
InsertBefore(v interface {}, mark * Element) *Element | 在 mark 点之前插入元素,mark 点由其他插入函数提供 |
PushBackList(other *List) | 添加 other 列表元素到尾部 |
PushFrontList(other *List) | 添加 other 列表元素到头部 |
删除元素
直接上示例
l := list.New()
l.PushBack("Green")
e := l.PushBack("Hello")
l.InsertBefore("Green", e)
// 删除
l.Remove(e)
列表的遍历
l := list.New()
l.PushBack("World!")
l.PushFront("Hello ")
// Front获取队列首个元素
for i := l.Front(); i != nil; i = i.Next(){
fmt.Println(i.value)
}
nil
是Go语言中的一个预定义的表示夫,它与其他语言的 null
有些许区别
nil
的特点:
nil
标识符不能比较nil
不是关键字或保留字nil
没有默认类型nil
是不一样的nil
占用的内存大小不同函数的基本组成:关键字 func
,函数名,参数列表,返回值,函数体,返回语句
Go语言中的三种类型的函数:
普通函数
fun 函数名(形参列表)(返回值列表){
函数体
}
例子:
func testFunc(a, b int, c, d string){
fmt.Prinln(a, b, c, d)
}
func testFunc2() (int int){
return 1, 2
}
func testFunc3() (a, b int){
a = 1
b = 2
return
}
在Go中,函数也是一种类型,可以保存在变量中
func getF(){
fmt.Prinln("You Guess!")
}
func main(){
var f func()
f = getF()
f()
}
定义
func (参数列表) (返回参数列表){
函数体
}
匿名函数的定义使用:
匿名函数作回调函数
func testFunc(a []int, f func(int)){
for _, v := range a{
f(v)
}
}
func main(){
testFunc([]int{1, 2, 3}, func(a int){
fmt.Prinln(a)
})
}
匿名函数实现封装
用 map
封装函数
函数与接口
函数的声明不能直接实现接口,需要将函数定义为类型后,使用类型设计结构体
Go语言中的闭包 = 函数+引用环境
可变参数
可变参数:函数传入的参数个数可变
func testFunc(args ...int){
for _, arg := range args{
fmt.Println(arg)
}
}
// 函数调用
func(1, 2, 3)
func(10, 20)
任意类型的可变参数
想要将函数的参数改为任意类型,只需要将类型指定为 interface{}
func testFunc(args ...interface{}){
}
defer
语句即将它所跟随的语句进行延迟处理,先被 defer
的语句最后被执行,最后被 defer
的语句最早执行
fmt.Println("Hello World!")
defer fmt.Prinln("C/C++")
defer fmt.Prinln("Python")
defer fmt.Prinln("GoLang")
defer fmt.Prinln("Java")
/*
执行顺序如下:
Java
GoLang
Python
C/C++
*/
使用 defer
在函数退出时释放资源
使用延迟并发解锁
使用延迟释放文件
Go语言的错误处理思想及设计包含以下特征:
error
),如果调用是成功的,错误接口将返回 nil
,否则返回错误。错误接口的定义
error
是Go声明的接口类型
type error interface{
Error() string
}
自定义错误
在Go语言中,使用 errors
包进行错误的定义,方式如下:
var err = errors.New("This is an error of program!") // 参数为报错信息
一般而言,当宕机发生时,程序会中断运行,并立即执行在该 goroutine
(可以先理解成线程)中被延迟的函数(defer
机制)。随后,程序崩溃并输出日志信息,日志信息包括 panic value
和函数调用的堆栈跟踪信息,panic value
通常是某种错误信息。
手动触发宕机
Go程序在宕机时,会将堆栈和 goroutine
信息输出到控制台
func main(){
panic("宕机")
}
/*
panic: 宕机
goroutine 1 [running]:
main.main()
*/
func panic(v interface{}}) // 参数可以是任意类型
宕机时出发延迟执行语句
程序 panic()
触发宕机发生时,panic()
后的代码不会被运行
recover
是Go的一个内置函数,可以让 goroutine
从宕机状态中恢复过来,recover
只在延迟函数 defer
中有效,在一般情况下,调用 recover
返回 nil
panic
和 recover
的关系
panic
没 recover
,程序宕机。panic
也有 recover
,程序不会宕机,执行完对应的 defer
后,从宕机点退出当前函数后继续执行。函数的运行时间是一个函数性能的指标,在Go语言中,可以使用 time
中的 Since()
函数来获取函数的运行时间
func Since(t Time) Duration
// time.Now().Sub(t)
func testFunc(){
begin := time.Now()
fmt.Println("Hello World!")
cost := time.Since(begin)
// cost := time.Now().Sub(begin)
fmt.Println(cost)
}
Go语言中提供 testing
包实现单元测试功能
测试需要:
命名文件需要以 _test.go
结尾
单个测试源文件可以有多个函数,每个测试函数需要以 Test
为前缀
编写测试用例的注意点:
_test.go
结尾;testing
包;Test
或 Benchmark
开头,后面可以跟任意字母组成的字符串,但第一个字母必须大写,例如 TestFunc007()
,一个测试用例文件中可以包含多个测试函数;(t *testing.T)
作为参数,性能测试以 (t *testing.B)
做为参数;go test
命令来执行,源码中不需要 main()
函数作为入口,所有以 _test.go
结尾的源码文件内以 Test
开头的函数都会自动执行。单元测试
//
package test
func Get(v int) int {
return v
}
//
package test
import "testing"
func TestGet(t *testing.T) {
v := Get(1)
if v != 2 {
t.Error("测试failed")
}
}
//
package test
import "testing"
func TestGet(t *testing.T) {
v := Get(1)
if v != 1 {
t.Error("测试failed")
}
}
// 命令行输入 go test -v
性能测试
//
func BenchmarkGet(b *testing.B) {
for i := 0; i < b.N; i++ {
Get(i)
}
}
// 命令函输入 go test -bench="."
覆盖率测试
覆盖率测试能知道测试程序总共覆盖了多少业务代码
# 命令行参数
go test -cover
结构体是一种自定义的新类型
结构体可以通过使用 new
来创建实例 [指针]
结构体的特性:
type 结构体名称 struct{
字段名称 字段类型
字段名称 字段类型
...
}
//
type Person struct{
Name string
Sex string
}
//
type Color struct{
Red, Green byte
}
结构体的定义是一种内存布局的描述,当结构体实例化时,才会分配内存
实例化格式
结构体时一种类型,通过 var
的方式声明结构体即可创建实例
// T--结构体,t--结构体实例
var t T
type Person struct{
Name string
Sex string
}
var p Person
p.Name = "Levin"
p.Sex = "male"
创建指针类型的结构体
Go语言中,可以使用 new
关键字对类型进行实例化,实例化后会生成指针类型的结构体
t := new(Person)
通过 new
创建的结构体实例,虽然类型为指针类型,但访问依旧可以通过 t.XXX
访问,Go采用了语法糖技术,将 t.xxx
的形式 (*t).xxx
取地址实例化
t := &T{}
type Person struct{
Name string
Sex string
}
p := &Person{}
p.Name = "Levin"
p.Sex = "male"
初始化有两种形式
键值对初始化
//
t := 结构体名{
字段一: 字段一的值, // 注意此处有逗号
字段二: 字段二的值,
...
}
多个值的列表初始化
//
t := 结构体名{
字段一的值, // 注意此处有逗号
字段二的值,
...
}
此种初始化方式的注意点:
必须初始化所有字段
每个初始值的填充顺序不得改变
键值对与此处初始化方式不能混用
匿名结构体
type 结构体名 struct{
xxx xxxType
xxx xxxType
xxx xxxType
...
}{
// 初始化
xxx: xxxValue,
xxx: xxxValue,
xxx: xxxValue,
...
}
结构体可以包含一个或多个匿名(即内嵌)字段,此时类型就是该字段的名字
type Point struct{
X int
Y int
}
type Line struct{
Point
}
l := new(Line){
Point{1, 1}
}
/*
l := new(Line)
l.Point.X = 1
l.Point.Y = 1
*/
结构体内嵌的特性:
初始化内嵌匿名结构体
type Point struct{
X, Y int
}
type Line struct{
Point
Point2 struct{
X, Y int
}
}
l := new(Line){
Point{1, 1},
struct{
X, Y int
}{
2,
2,
}
}
当名字发生冲突时,编译器会编译失败,zer`
Go语言自带垃圾回收机制(GC)。
GC 通过独立的进程执行,它会搜索不再使用的变量,并将其释放。需要注意的是,GC 在运行时会占用机器资源。
GC 是自动进行的,如果要手动进行 GC,可以使用 runtime.GC()
函数,显式的执行 GC。显式的进行 GC 只在某些特殊的情况下才有用,比如当内存资源不足时调用 runtime.GC()
,这样会立即释放一大片内存,但是会造成程序短时间的性能下降。
finalizer
(终止器)是与对象关联的一个函数,通过 runtime.SetFinalizer
来设置,如果某个对象定义了 finalizer
,当它被 GC 时候,这个 finalizer
就会被调用,以完成一些特定的任务,例如发信号或者写日志等。
注意:终止器只会在对象被 GC 时执行
设置终止器的函数:
func SetFinalizer(x, f interface{})
/*
x -- 通过new申请的对象指针/通过取址得到的指针
f -- 函数,接收x类型的参数
*/
runtime.SetFinalizer
函数只会在 x
不能使用的任意时间被调用执行
接口类型是对其它类型行为的抽象和概括
接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让我们的函数更加灵活和更具有适应能力
接口是双方约定的一种合作协议,接口实现者不需要关心接口会被怎样使用,调用者也不需要关心接口的实现细节。
接口是一种类型,也是一种抽象结构,不会暴露所含数据的格式、类型及结构。
声明
type 接口名 interface{
方法名(参数列表) 返回值列表
方法名(参数列表) 返回值列表
方法名(参数列表) 返回值列表
...
}
//
type Writer interface{
Write(p []byte) (n int, err error)
}
// 类比 toString()
type Stringer interface {
String() string
}
Go语言中没有 implements
关键字,编译器在需要时候会自动检查两个类型的实现关系
接口被实现的条件:
在Go语言中类型和接口之间有一对多和多对一的关系
一个类型可以实现多个接口
一个类型可以同时实现多个接口,而接口间彼此独立,不知道对方的实现。
多个类型可以实现相同的接口
类型断言是使用在接口值上的操作,用于检查接口类型变量的值是否实现了具体的类型
value, ok := i.(T)
/*
i -- 接口
T -- 具体类型
返回值为i和布尔值
*/
Go语言可以使用接口断言实现将一个接口转为另一个接口或类型
空接口是接口类型的特殊形式,空接口没有任何方法,因此任何类型都无须实现空接口。从实现的角度看,任何值都满足这个接口的需求。因此空接口类型可以保存任何值,也可以从空接口中取出原值。
将值保存到空接口中
var a int = 10
var ii interface{}
ii = a
从空接口获取值
var a int = 10
var ii interface{} = a
// var b int = ii 编译错误,不能直接赋值
var b int = ii.(int) // 需要使用断言
空接口值的比较
空接口比较直接使用 ==
即可比较
类型不同的空接口的值比较结果不同
不能比较空接口中的动态值 [map
、切片]
type-switch
流程控制
switch 接口变量.(type){
case 类型:
// code
case 类型:
// code
case 类型:
// code
...
default:
// code
}
error
接口中有一个方法 Error() string
,所有实现该接口的类型都可以作为一个错误类型
创建一个 error
实例,通过调用 errors.New()
函数,创建一个实例
自定义错误类型
通过调用 Error() string
方法自定义错误信息
Go语言是使用包来组织源代码的,包(package
)是多个 Go 源码的集合,是一种高级的代码复用方案。Go语言中为我们提供了很多内置包,如 fmt
、os
、io
等。
任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是package pacakgeName
语句,通过该语句声明自己所在的包。
一般情况下,包名即源文件所在目录名,包的用法如下:
一般情况下,包名是小写的
一般情况下,包名与所在目录同名
包名不能包含 -
等特殊符号
包一般使用域名作为目录名称 [同Java]
包名为 main
的包为程序入口包,编译时若不包含 main
包的源文件不会得到可执行文件
同一个目录下的源文件同属一个包
包的导入
// 单行导入
import "fmt"
// 多行导入
import(
"fmt"
"errors"
)
// 全路径导入
// 相对路径导入
包的引用
import "fmt"
fmt.Println("Hello World!")
// 自定义别名引用格式
import F "fmt"
F.Println("Hello World!")
// 省略引用格式
import . "fmt"
Println("Hello World!")
// 匿名引用格式
// 引用某个包只是为了执行包初始化的init函数,可以采取使用匿名引用格式
import _ "fmt"
init
函数,包加载时会执行所有的 init
函数init
函数时不保证执行顺序init
函数只执行一次包加载
Go语言包的初始化有如下特点:
main
函数引用的包开始,逐级查找包的引用,直到找到没有引用其他包的包,最终生成一个包引用的有向无环图。init
函数。在Go语言中的封装也与其他语言的封装类似,只是实现方法不同,其他语言有关键字来声明属性的访问权限,Go语言通过字段名称的首字母来实现
实现:
Set
方法Get
方法GOPATH 是 Go语言中使用的一个环境变量,它使用绝对路径提供项目的工作目录。
GOPATH 处理大量Go源码、多个包组合而成的复杂工程
GOPATH的工程结构
在GOPATH指定的工作目录下,代码总会保存在 GOPATH/src
的目录下,在编译后将二进制可执行文件放在 GOPATH/bin
的目录下,生成的缓存文件保存在 GOPATH/pkg
下。
使用 import
将包导入
程序启动前包初始化入口:init
init()
函数的特性如下:
init()
函数。init()
函数会在程序执行前(main()
函数执行前)被自动调用。main()
中引用的包,以深度优先顺序初始化。go module
是Go语言从 1.11 版本之后官方推出的版本管理工具,并且从 Go1.13 版本开始,go module
成为了Go语言默认的依赖管理工具。
Moudules
Modules是Go包的集合,是源代码交换和版本控制的单元
使用:设置 GO111MODULE
# 禁用go module,编译时会从 GOPATH 和 vendor 文件夹中查找包;
GO111MODULE=off
# 启用go module,编译时会忽略 GOPATH 和 vendor 文件夹,只根据 go.mod下载依赖
GO111MODULE=off
# 当项目在 GOPATH/src 目录之外,并且项目根目录有 go.mod 文件时,开启 go module
GO111MODULE=auto #(默认值)
# Windows
set GO111MODULE=on
set GO111MODULE=auto
# Linux
export GO111MODULE=on
export GO111MODULE=auto
go mod
常用命令:
go mod download
:下载依赖包到本地go mod edit
:编辑 go.mod
文件go mod init
:初始化当前文件夹,创建 go.mod
文件go mod tidy
:增加缺少的包,删除无用包GOPROXY
代理服务器,由于服务器在国外,无法直接通过 go get
方法,故需要设置代理,两个常用代理服务器:
使用go get命令下载指定版本的依赖包
执行go get
命令,在下载依赖包的同时还可以指定依赖包的版本。
go get -u
命令会将项目中的包升级到最新的次要版本或者修订版本;go get -u=patch
命令会将项目中的包升级到最新的修订版本;go get [包名]@[版本号]
命令会下载对应包的指定版本或者将对应包升级到指定的版本。并发/并行
多线程程序在单核心的 cpu 上运行,称为并发;多线程程序在多核心的 cpu 上运行,称为并行。
协程/线程
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
线程:一个线程上可以跑多个协程,协程是轻量级的线程。
Goroutine
Goroutine是一种轻量级的线程实现,是Go语言设计的核心
Go语言内部实现了 Goroutine 之间的内存共享
使用 go
关键字就可以创建 Goroutine,将 go
声明放到一个需调用的函数之前,在相同地址空间调用运行这个函数,这样该函数执行时便会作为一个独立的并发线程,这种线程在Go语言中则被称为 Goroutine。
并发通信
在工程中,两种最常用的并发通信模型:共享数据和消息(队列)
Go语言中提供的是通信模型,以消息机制作为通信方式
消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,在不同并发单元间变量不共享。
Go中的消息通信机制被称为 channel
在并发的程序中,存在资源竞争,如果两个或多个 goroutine 在没有互相同步的情况下,访问某个共享的资源,如并发中最常见的读写问题,就会处于竞争的状态,这就是资源竞争。
// 上锁
var (
count int
lock sync.Mutex
)
func PrimeNumber(n int) {
for i := 2; i < n; i++ {
if n%i == 0 {
return
}
}
fmt.Printf("%v\t", n)
// 写操作上锁
lock.Lock()
count++
// 写操作完成后解锁
lock.Unlock()
}
func Goroutine() {
for i := 2; i < 100001; i++ {
// PrimeNumber(i)
// 开启协程
go PrimeNumber(i)
}
var key string
fmt.Scanln(&key)
fmt.Printf("一共有%v个素数", count)
}
注:此处已经用锁 mutex
实现了同步,若不加锁,会导致读写操作的结果被覆盖
对于同一个资源的读写必须是原子化的,即同一时刻只运行有一个 goroutine 对共享资源进行访问
同步机制
Go语言中的 atomic
和 sync
包中的函数可以对共享资源加锁
原子函数
Go语言中存在原子函数,能够以低层的加锁机制来同步访问整型变量和指针(类比Java中的集合)
import "sync/atomic"
var c = 0;
// 安全对变量c进行操作,不会发生覆盖的情况
atomic.AddInt64(&c, 1)
//
atomic.LoadInt64(&c)
//
atomic.StoreInt64(&c, 10)
互斥锁
除了使用原子函数外,还可以使用互斥锁对资源进行同步
// 上锁
var (
count int
lock sync.Mutex
)
func PrimeNumber(n int) {
for i := 2; i < n; i++ {
if n%i == 0 {
return
}
}
fmt.Printf("%v\t", n)
// 写操作上锁
lock.Lock()
count++
// 写操作完成后解锁
lock.Unlock()
}
func Goroutine() {
for i := 2; i < 100001; i++ {
// PrimeNumber(i)
// 开启协程
go PrimeNumber(i)
}
var key string
fmt.Scanln(&key)
fmt.Printf("一共有%v个素数", count)
}
Go程序运行时实现了一个小型的任务调度器,可以通过 runtime.GOMAXPROCS()
函数维护线程池中的线程与CPU核心数量的关系:
//
runtime.GOMAXPROCS(逻辑CPU数量)
/*
<1:不改变任何数值
==1:单核执行
>1:多核并发执行
*/
//
runtime.GOMAXPROCS(runtime.NumCPU())
// runtime.NumCPU()获取CPU数量
Go语言在 GOMAXPROCS 数量与任务数量相等时,可以做到并行执行,但一般情况下都是并发执行。
Go语言中的 sync
包提供了互斥锁 Mutex
和读写锁 RWMutex
锁是 sync
中的核心,其主要实现依托两个方法:加锁(Lock
)、解锁(Unlock
)
Mutex锁
// 加锁
func (m *Mutex)Lock()
// 解锁
func (m *Mutex)Unlock()
RWMutex锁
经典的单写多读模型
// 写操作锁
func (*RWMutex)Lock
func (*RWMutex)Unlock
// 读操作锁
func (*RWMutex)RLock
func (*RWMutex)RUnlock
两者的区别:
即:
channel
是一个通信机制,可以让一个 goroutine 向另一个 goroutine 发送值信息,channel 的类型是不确定的,根据需要传递的值的类型来设置其数据类型,如:chan int
通道的特性
Go中的 channel
是一种特殊的类型,同锁一样,每时刻只能有一个 Goroutine 访问通道进行数据的发送或获取
通道遵循先入先出的原则,保证消息的顺序
通道的声明
var 通道变量名称 chan 通道类型
// chan的空值为nil,需要配合make使用
通道的创建
通道实例名称 := make(chan 数据类型)
//
c1 := make(chan int)
c2 := make(chan interface{}) // 由通道数据类型是空接口,故可以存放任意的数据类型
发送数据
channel
发送信息通过操作符 <-
,如下:
通道变量名称 <- 值
//
c := make(chan int)
c <- 1
c_new := make(chan interface{})
c <- "Hello World!"
注:当数据往 channel
发送时,一直没有接收,此时发送操作会阻塞
接收数据
channel
接收信息同样通过操作符 <-
,如下:
//
c := make(chan int)
c <- 0
a := <- c
// TODO 此种方式会造成很高的CPU占用,故一般不采用此种方法,而采用另一种方法 selet channel
c := make(chan int)
c <- 10
data, ok := <- c
if ok{
fmt.Println(data)
}else{
fmt.Println("Error!")
}
//
c := make(chan int)
c <- 10
<- c
注:
channel
发送信息,接收端持续阻塞循环接收
通道的数据可以通过 for...range
语句实现多个元素的接收:
for data := range ch{
fmt.Println(data)
}
Go语言中提供了单方向的 channel
类型,单向 channel
只能用于写入或读取一个功能
单向通道的声明
当一个函数只对 channel
进行读取或者只进行写入时,可以将该通道参数设置为单向通道类型,从而限制该函数对该 channel
的操作
// 只能写入
var 通道变量名 chan <- 数据类型
// 只能读取
var 通道变量名 <- chan 数据类型
// 赋值初始化
c := make(chan int)
var chSendOnly chan <- int = c
var chRecvOnly <- chan int = c
// 也可以创建只读或只写的通道,但实际开发这样子的做法毫无作用
管道的关闭
close(ch) // ch为channel实例
// 判断是否关闭
v, ok := <- ch
无缓冲的通道,顾名思义即没有缓存池,需要接收方和发送方都做好准备
ch := make(chan int) // 即创建一个无缓冲的通道
Go语言中的缓存通道能够在接受前存储一个或多个值,该种通道只有在缓存区存放数据的空间接收方才会阻塞
有缓冲和无缓冲通道最大的区别为:
带缓冲通道的创建
//
通道变量名 := make(chan 数据类型, 缓冲大小)
//
ch := make(chan int, 10)
fmt.Println(len(ch)) // 通过内置函数查看当前通道的大小
ch <- 1
ch <- 5
ch <- 10
fmt.Println(len(ch))
/*
0
3
*/
带缓冲通道的阻塞
带缓冲通道在以下情况下会发生阻塞:
channel
机制
Go语言不提供直接对超时的处理机制,但能够通过 select
语句来设置超时
select 语句
select
语句和 switch
语句的语法相似,但 select
语句中最大的区别是每个 case
语句里必须是一个IO操作,即每个 case
都必须是一个通信
select{
case <- chan_01: //if get the data from chan_01, then go to code1
// code1
case chan_02 <- 10: //if write the data to chan_01, then go to code2
// code2
default:
//
}
在 select
语句中,Go语言会从第一个 case
语句执行到最后一个
如果存在多个 case
语句都满足,那么从这些可执行的语句中任选一条进行使用
如果不存在任意一个 case
语句满足,即有以下情况:
default
语句,此时会执行 default
语句default
语句,此时 select
语句被阻塞,等待有一个通道不再被阻塞Go语言除了使用 channel
和互斥锁来实现两个并发进程的同步外,还提供了等待组(WaitGroup
),存在于 sync
包中,每个等待组内部维护一个计数器,该计数器的默认值为0
WaitGroup
中的常用方法:
(wg *WaitGroup)Add(delta int) // 计数器+1
(wg *WaitGroup)Done() // 计数器-1
(wg *WaitGroup)Wait() // 当计数器不为0时,发起阻塞,直到计数器为0
对于一个 WaitGroup
的实例 wg
:
wg.Add(delta)
来改变 wg
中计数器的值wg.Add(-1)
和 wg.Done()
等价wg.Wait()
,会出现两种情况:
noop
)死锁
死锁是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成互相等待
活锁
活锁是另一种形式的活跃性问题,该问题尽管不会阻塞线程,但也不能继续执行,因为线程将不断重复同样的操作,而且总会失败。
附:
该笔记由作者本人通过C语言中文网和观看B站视频提炼总结