时间:2021.06.22
环境:Windows
目的:
说明:
作者:Zhong QQ交流群:121160124 欢迎加入!
上面的示例中出现的问题怎么解决呢?加一个互斥锁 Mutex 就可以了
环境配置
新建文件夹src 创建一个go模块
go mod init example.com/src
在src目录下写代码即可
格式化输出变量
查看变量的字节大小、数据类型等信息
查看变量字节大小
var t2 string = "abc" fmt.Printf("t2占用的字节数为:%d\n", unsafe.Sizeof(t2))
查看变量类型
var t1 int = 100 fmt.Printf("t1的数据类型:%T\n", t1)
查看整形变量对应的unicode字符
var t3 int = 10086 fmt.Printf("t3对应的unicode为:%c\n", t3)
数据类型
常量
常量是一个简单值的标识符,在程序运行时,不会被修改的量。
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。
整形的类型
int有符号类型
int无符号类型
int其它的类型
字符串和字符
字符使用单引号定义的单个字节
var t4 byte = 'a' fmt.Printf("t4: %v\n", t4)
字符串是由多个字符连接组成的使用双引号/反引号定义
var t5, t6 string = "hi", `hello world` fmt.Printf("t5: %s t6: %s\n", t5, t6)
值类型和引用类型
所有像 int、float、bool 和 string 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值
当使用等号 = 将一个变量的值赋值给另一个变量时,如:j = i,实际上是在内存中将 i 的值进行了拷贝
值类型的变量的值存储在栈中
更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存。
一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。
这个内存地址为称之为指针,这个指针实际上也被存在另外的某一个字中。
同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。
当使用赋值语句 r2 = r1 时,只有引用(地址)被复制。
如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。
流程控制
条件语句
if
package main import "fmt" func main() { /* 局部变量定义 */ var a int = 100; /* 判断布尔表达式 */ if a < 60 { /* 如果条件为 true 则执行以下语句 */ fmt.Printf("a 小于 60\n" ); } else if a < 80 { fmt.Printf("a 小于 80 大于 60\n" ); } else { /* 如果条件为 false 则执行以下语句 */ fmt.Printf("a 不小于 80\n" ); } fmt.Printf("a 的值为 : %d\n", a); }
if简写
package main import "fmt" func main() { fruits := map[string]string{ "apple": "delicious", "banana": "coca", } if res, ok := fruits["apple"]; ok { fmt.Println(res, ok) } else { fmt.Println("None") } }
if...else
if嵌套
switch
switch 语句用于基于不同条件执行不同动作。
package main import "fmt" func main() { /* 定义局部变量 */ var grade string = "B" var marks int = 90 switch marks { case 90: grade = "A" case 80: grade = "B" case 50,60,70 : grade = "C" default: grade = "D" } switch { case grade == "A" : fmt.Printf("优秀!\n" ) case grade == "B", grade == "C" : fmt.Printf("良好\n" ) case grade == "D" : fmt.Printf("及格\n" ) case grade == "F": fmt.Printf("不及格\n" ) default: fmt.Printf("差\n" ); } fmt.Printf("你的等级是 %s\n", grade ); }
用于判断类型的实例
package main import "fmt" func main() { var x interface{} switch i := x.(type) { case nil: fmt.Printf(" x 的类型 :%T",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("未知型") } }
select
select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。
package main import "fmt" func main() { var c1, c2, c3 chan int var i1, i2 int select { case i1 = <-c1: fmt.Printf("received ", i1, " from c1\n") case c2 <- i2: fmt.Printf("sent ", i2, " to c2\n") case i3, ok := (<-c3): // same as: i3, ok := <-c3 if ok { fmt.Printf("received ", i3, " from c3\n") } else { fmt.Printf("c3 is closed\n") } default: fmt.Printf("no communication\n") } }
循环语句
循环处理语句
for循环
Go语言的For循环有3中形式,只有其中的一种使用分号。
for init; condition; post { } for condition { } for { }
package main import "fmt" func main() { var b int = 15 var a int numbers := [6]int{1, 2, 3, 5} /* for 循环 */ for a := 0; a < 10; a++ { fmt.Printf("a 的值为: %d\n", a) } for a < b { a++ fmt.Printf("a 的值为: %d\n", a) } for i,x:= range numbers { fmt.Printf("第 %d 位 x 的值 = %d\n", i,x) } }
循环嵌套
无限循环
如果循环中条件语句永远不为 false 则会进行无限循环,我们可以通过 for 循环语句中只设置一个条件表达式来执行无限循环
package main import "fmt" func main() { for true { fmt.Printf("这是无限循环。\n"); } }
控制语句
break 语句
经常用于中断当前 for 循环或跳出 switch 语句
continue 语句
跳过当前循环的剩余语句,然后继续进行下一轮循环。
goto 语句
将控制转移到被标记的语句。
defer
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
package main import "fmt" func main() { defer fmt.Println("world") fmt.Println("hello") }
defer 栈
推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
package main import "fmt" func main() { fmt.Println("counting") for i := 0; i < 10; i++ { defer fmt.Println(i) } fmt.Println("done") }
顺序控制
从上到下的顺序执行代码
分支控制
if else
switch
循环控制
for
while
函数
Go 语言最少有个 main() 函数。
函数参数
函数如果使用参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数:
值传递
值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
引用传递
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
函数用法
函数作为值
函数定义后可作为值来使用
闭包
闭包是匿名函数,可在动态编程中使用
方法
Go 没有类。不过你可以为结构体类型定义方法。
方法就是一个包含了接受者的函数
Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集
方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。
在此例中,Abs 方法拥有一个名为 v,类型为 Vertex 的接收者。
package main import ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main() { v := Vertex{3, 4} fmt.Println(v.Abs()) }
实例
package main import ( "fmt" ) /* 定义函数 */ type Circle struct { radius float64 } func main() { var c1 Circle c1.radius = 10.00 fmt.Println("Area of Circle(c1) = ", c1.getArea()) } //该 method 属于 Circle 类型对象中的方法 func (c Circle) getArea() float64 { //c.radius 即为 Circle 类型对象中的属性 return 3.14 * c.radius * c.radius }
数组
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。
Go语言中的数组可以在声明后再赋值 也可以在声明时初始化值 可以改变元素的值
声明数组
var arr1 [10] int // 默认数组中的元素全是0 arr1[0] = 10 // 指定数组中的第一个元素值为10
初始化数组
几种定义数组赋值的例子
package main import "fmt" func main () { var arr2 = [10] int {1,2,3,4,5,6,7} var arr3 = [] int {1, 2} var arr4 = [] int {} var arr5 = [10] int {0:0, 2:20, 5:50} arr5[7] = 70 fmt.Println(arr2) fmt.Println(arr3) fmt.Println(arr4) fmt.Println(arr5) }
数组完整操作
数组完整操作(声明、赋值、访问)的实例
package main import "fmt" func main() { var n [10]int /* n 是一个长度为 10 的数组 */ var i,j int /* 为数组 n 初始化元素 */ for i = 0; i < 10; i++ { n[i] = i + 100 /* 设置元素为 i + 100 */ } /* 输出每个数组元素的值 */ for j = 0; j < 10; j++ { fmt.Printf("Element[%d] = %d\n", j, n[j] ) } }
向函数传递数组
在函数定义时,声明形参为数组,我们可以通过以下两种方式来声明:
形参设定数组大小
void myFunction(param [10]int){ . . . }
形参未设定数组大小
void myFunction(param []int){ . . . }
指针
一个指针变量可以指向任何一个值的内存地址
声明指针
var ip *int /* 指向整型*/ var fp *float32 /* 指向浮点型 */
使用指针
指针使用流程:
- 定义指针变量。
- 为指针变量赋值。
- 访问指针变量中指向地址的值。
在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。
package main import "fmt" func main() { var a int= 20 /* 声明实际变量 */ var ip *int /* 声明指针变量 */ ip = &a /* 指针变量的存储地址 */ fmt.Printf("a 变量的地址是: %x\n", &a ) /* 指针变量的存储地址 */ fmt.Printf("ip 变量的存储地址: %x\n", ip ) /* 使用指针访问值 */ fmt.Printf("*ip 变量的值: %d\n", *ip ) }
空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
nil 指针也称为空指针。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
一个指针变量通常缩写为 ptr。
指针数组
var ptr [10]*int;
指向指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址
指向指针的指针变量声明
var ptr **int;
访问指向指针的指针变量值需要使用两个 * 号
指针作为函数参数
Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。
package main import "fmt" func main() { /* 定义局部变量 */ var a int = 100 var b int= 200 fmt.Printf("交换前 a 的值 : %d\n", a ) fmt.Printf("交换前 b 的值 : %d\n", b ) /* 调用函数用于交换值 * &a 指向 a 变量的地址 * &b 指向 b 变量的地址 */ swap(&a, &b); fmt.Printf("交换后 a 的值 : %d\n", a ) fmt.Printf("交换后 b 的值 : %d\n", b ) } func swap(x *int, y *int) { var temp int temp = *x /* 保存 x 地址的值 */ *x = *y /* 将 y 赋值给 x */ *y = temp /* 将 temp 赋值给 y */ }
结构体
Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
定义结构体
结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体有一个或多个成员。type 语句设定了结构体的名称。
type Person struct { name string; age int; score float32; }
使用上面定义的结构体声明变量
LiuXiang := Person{"Liuxiang", 18, 12.88}
匿名结构体
package main import "fmt" func main() { person:= struct { // 匿名结构体 name string age int }{name:"匿名",age:1} fmt.Println("person:",person) }
访问结构体成员
要访问结构体成员,需要使用点号 (.) 操作符,格式为:"结构体.成员名"。
package main import "fmt" type Books struct { title string author string subject string book_id int } func main() { var Book1 Books /* 声明 Book1 为 Books 类型 */ /* book 1 描述 */ Book1.title = "Go 语言" Book1.author = "www.w3cschool.cn" Book1.subject = "Go 语言教程" Book1.book_id = 6495407 /* 打印 Book1 信息 */ fmt.Printf( "Book 1 title : %s\n", Book1.title) fmt.Printf( "Book 1 author : %s\n", Book1.author) fmt.Printf( "Book 1 subject : %s\n", Book1.subject) fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id) }
结构体作为函数参数
package main import "fmt" type Books struct { title string author string subject string book_id int } func main() { var Book1 Books /* 声明 Book1 为 Books 类型 */ Book1.title = "Go 语言" Book1.author = "www.w3cschool.cn" Book1.subject = "Go 语言教程" Book1.book_id = 6495407 printBook(Book1) } func printBook( book Books ) { fmt.Printf( "Book title : %s\n", book.title); fmt.Printf( "Book author : %s\n", book.author); fmt.Printf( "Book subject : %s\n", book.subject); fmt.Printf( "Book book_id : %d\n", book.book_id); }
结构体指针
定义指向结构体的指针
var Book_Ptr *Books
上面定义的指针变量可以存储结构体变量的地址。查看结构体变量地址,可以将 & 符号放置于结构体变量前
Book_Ptr = &Book1;
实例
package main import "fmt" type Books struct { title string author string subject string book_id int } func main() { var Book1 Books /* Declare Book1 of type Book */ var Book2 Books /* Declare Book2 of type Book */ /* book 1 描述 */ Book1.title = "Go 语言" Book1.author = "www.w3cschool.cn" Book1.subject = "Go 语言教程" Book1.book_id = 6495407 /* book 2 描述 */ Book2.title = "Python 教程" Book2.author = "www.w3cschool.cn" Book2.subject = "Python 语言教程" Book2.book_id = 6495700 /* 打印 Book1 信息 */ printBook(&Book1) /* 打印 Book2 信息 */ printBook(&Book2) } func printBook( book *Books ) { fmt.Printf( "Book title : %s\n", book.title); fmt.Printf( "Book author : %s\n", book.author); fmt.Printf( "Book subject : %s\n", book.subject); fmt.Printf( "Book book_id : %d\n", book.book_id); }
Note
结构体实例类似于Python中的字典 但又不一样
结构体通过实现方法可实现类似于面向对象编程语言中类功能
切片(Slice)
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
定义切片
声明一个未指定大小的数组来定义切片 切片不需要说明长度
var identifier []type
使用make()函数来创建切片
//var slice1 []type = make([]type, len) //也可以简写为 //slice1 := make([]type, len) //可以指定容量 其中capacity为可选参数 这里len是数组的长度并且也是切片的初始长度 //make([]T, length, capacity)
切片初始化
直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s :=[] int {1,2,3}
初始化切片s,是数组arr的引用
s := arr[startIndex:endIndex]
通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片
s :=make([]int,len,cap)
len() 和 cap() 函数
切片是可索引的,并且可以由 len() 方法获取长度。
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
package main import "fmt" func main() { var numbers = make([]int,3,5) printSlice(numbers) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
空(nil)切片
一个切片在未初始化之前默认为 nil,长度为 0
var numbers []int
切片截取
通过设置下限及上限来设置截取切片 [lower-bound:upper-bound]
package main import "fmt" func main() { /* 创建切片 */ numbers := []int{0,1,2,3,4,5,6,7,8} printSlice(numbers) /* 打印原始切片 */ fmt.Println("numbers ==", numbers) /* 打印子切片从索引1(包含) 到索引4(不包含)*/ fmt.Println("numbers[1:4] ==", numbers[1:4]) /* 默认下限为 0*/ fmt.Println("numbers[:3] ==", numbers[:3]) /* 默认上限为 len(s)*/ fmt.Println("numbers[4:] ==", numbers[4:]) numbers1 := make([]int,0,5) printSlice(numbers1) /* 打印子切片从索引 0(包含) 到索引 2(不包含) */ number2 := numbers[:2] number2[0] = 10 //会同时改变numbers printSlice(number2) /* 打印子切片从索引 2(包含) 到索引 5(不包含) */ number3 := numbers[2:5] printSlice(number3) printSlice(numbers) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
append() 和 copy() 函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。
package main import "fmt" func main() { var numbers []int printSlice(numbers) /* 允许追加空切片 */ numbers = append(numbers, 0) printSlice(numbers) /* 向切片添加一个元素 */ numbers = append(numbers, 1) printSlice(numbers) /* 同时添加多个元素 */ numbers = append(numbers, 2,3,4) printSlice(numbers) /* 创建切片 numbers1 是之前切片的两倍容量*/ numbers1 := make([]int, len(numbers), (cap(numbers))*2) /* 拷贝 numbers 的内容到 numbers1 */ copy(numbers1,numbers) printSlice(numbers1) } func printSlice(x []int){ fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
Note:
新切片的cap为原切片的cap-新切片的startIndex
当新的切片len比原来的切片cap大时 新切片的cap将增加一倍
startIndex不能大于endIndex Index不能大于长度
范围(Range)
Go 语言中 range 关键字用于for循环中迭代数组(array)、切片(slice)、链表(channel)或集合(map)的元素。在数组和切片中它返回元素的索引值,在集合中返回 key-value 对的 key 值。
package main import "fmt" func main() { //这是我们使用range去求一个slice的和。使用数组跟这个很类似 nums := []int{2, 3, 4} sum := 0 for _, num := range nums { sum += num } fmt.Println("sum:", sum) //在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。 for i, num := range nums { if num == 3 { fmt.Println("index:", i) } } //range也可以用在map的键值对上。 kvs := map[string]string{"a": "apple", "b": "banana"} for k, v := range kvs { fmt.Printf("%s -> %s\n", k, v) } //range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。 for i, c := range "go" { fmt.Println(i, c) } }
Map(集合)
在Go语言里Map的概念相当于Python语言里的Dict
Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。
定义 Map
可以使用内建函数 make 也可以使用 map 关键字来定义 Map 如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对
/* 使用map关键字 声明变量,默认 map 是 nil */ var map_variable map[key_data_type]value_data_type /* 使用 make 函数 */ map_variable = make(map[key_data_type]value_data_type)
实例
package main import "fmt" func main() { //I.先声明一个nil map 再使用make初始化然后赋值 var countryCapitalMap map[string]string /* 创建集合 */ countryCapitalMap = make(map[string]string) /* map 插入 key-value 对,各个国家对应的首都 */ countryCapitalMap["France"] = "Paris" countryCapitalMap["Italy"] = "Rome" //II. 直接使用make创建再赋值 //countryCapitalMap := make(map[string]string) //countryCapitalMap["France"] = "Paris" //countryCapitalMap["Italy"] = "Rome" //III. 直接使用map创建、初始化并赋值 //countryCapitalMap := map[string]string{ // "France": "Paris", // "Italy": "Rome", //} /* 使用 key 输出 map 值 */ for country := range countryCapitalMap { fmt.Println("Capital of",country,"is",countryCapitalMap[country]) } /* 查看元素在集合中是否存在 */ captial, ok := countryCapitalMap["United States"] /* 如果 ok 是 true, 则存在,否则不存在 */ if(ok){ fmt.Println("Capital of United States is", captial) }else { fmt.Println("Capital of United States is not present") } }
delete() 函数
package main import "fmt" func main() { /* 创建 map */ countryCapitalMap := map[string] string {"France":"Paris","Italy":"Rome","Japan":"Tokyo","India":"New Delhi"} fmt.Println("原始 map") /* 打印 map */ for country := range countryCapitalMap { fmt.Println("Capital of",country,"is",countryCapitalMap[country]) } /* 删除元素 */ delete(countryCapitalMap,"France"); fmt.Println("Entry for France is deleted") fmt.Println("删除元素后 map") /* 打印 map */ for country := range countryCapitalMap { fmt.Println("Capital of",country,"is",countryCapitalMap[country]) } }
Note
使用map关键字创建和声明的Map默认为nil 不能增加元素和赋值 需要使用make方法初始化之后使用
使用make方法创建和声明的Map是初始化的Map可以做赋值操作
递归函数
递归,就是在运行的过程中调用自己。
Go 语言支持递归。但我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中。
递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等。
func recursion() { recursion() /* 函数调用自身 */ } func main() { recursion() }
阶乘
package main import "fmt" func Factorial(x int) (result int) { if x == 0 { result = 1; } else { result = x * Factorial(x - 1); } return; } func main() { var i int = 15 fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(i)) }
斐波那契数列
package main import "fmt" func fibonacci(n int) int { if n < 2 { return n } return fibonacci(n-2) + fibonacci(n-1) } func main() { var i int for i = 0; i < 10; i++ { fmt.Printf("%d\t", fibonacci(i)) } }
类型转换
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。
实例
package main import "fmt" func main() { var sum int = 17 var count int = 5 var mean float32 mean = float32(sum)/float32(count) fmt.Printf("mean 的值为: %f\n",mean) }
go 不支持隐式转换类型
如下是不能通过编译的
package main import "fmt" func main() { var a int64 = 3 var b int32 b = a fmt.Printf("b 为 : %d", b) }
改成 b = int32(a)就可以
package main import "fmt" func main() { var a int64 = 3 var b int32 b = int32(a) fmt.Printf("b 为 : %d", b) }
接口
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
接口是一组仅包含方法名、参数、返回值的未具体实现的方法的集合。
实例
package main import ( "fmt" ) type Phone interface { call() } type NokiaPhone struct { } func (nokiaPhone NokiaPhone) call() { fmt.Println("I am Nokia, I can call you!") } type IPhone struct { } func (iPhone IPhone) call() { fmt.Println("I am iPhone, I can call you!") } func main() { var phone Phone phone = new(NokiaPhone) phone.call() phone = new(IPhone) phone.call() }
Note
接口的概念类似于Python中的多态
如果实现了接口的所有方法,则认为实现了该接口,无需在该类型上显示的添加声明。
错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义
type error interface { Error() string }
我们可以在编码中通过实现 error 接口类型来生成错误信息。函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息
func Sqrt(f float64) (float64, error) { if f < 0 { return 0, errors.New("math: square root of negative number") } // 实现 }
实例
package main import ( "fmt" ) // 定义一个 DivideError 结构 type DivideError struct { dividee int divider int } // 实现 `error` 接口 func (de *DivideError) Error() string { strFormat := ` Cannot proceed, the divider is zero. dividee: %d divider: 0 ` return fmt.Sprintf(strFormat, de.dividee) } // 定义 `int` 类型除法运算的函数 func Divide(varDividee int, varDivider int) (result int, errorMsg string) { if varDivider == 0 { dData := DivideError{ dividee: varDividee, divider: varDivider, } errorMsg = dData.Error() return } else { return varDividee / varDivider, "" } } func main() { // 正常情况 if result, errorMsg := Divide(100, 10); errorMsg == "" { fmt.Println("100/10 = ", result) } // 当被除数为零的时候会返回错误信息 if _, errorMsg := Divide(100, 0); errorMsg != "" { fmt.Println("errorMsg is: ", errorMsg) } }
sync包与锁
限制线程对变量的访问
sync 包里提供了互斥锁 Mutex 和读写锁 RWMutex 用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况
锁是 sync 包中的核心,它主要有两个方法,分别是加锁(Lock)和解锁(Unlock)。
互斥锁 Mutex
在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量。
不使用锁时,在并发的情况下可能无法得到想要的结果
package main import ( "fmt" "time" ) func main() { var a = 0 for i := 0; i < 1000; i++ { go func(idx int) { a += 1 fmt.Println(a) }(i) } time.Sleep(time.Second) }
上面的示例中出现的问题怎么解决呢?加一个互斥锁 Mutex 就可以了
package main import ( "fmt" "sync" "time" ) func main() { var a = 0 var lock sync.Mutex for i := 0; i < 1000; i++ { go func(idx int) { lock.Lock() defer lock.Unlock() a += 1 fmt.Printf("goroutine %d, a=%d\n", idx, a) }(i) } // 等待 1s 结束主程序 // 确保所有协程执行完 time.Sleep(time.Second) }
读写锁
读写锁有如下四个方法:
- 写操作的锁定和解锁分别是func (*RWMutex) Lock和func (*RWMutex) Unlock;
- 读操作的锁定和解锁分别是func (*RWMutex) Rlock和func (*RWMutex) RUnlock。
读写锁的区别在于:
- 当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
- 当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;
- 当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。
所以说这里的读锁定(RLock)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)。
我们可以将其总结为如下三条:
- 同时只能有一个 goroutine 能够获得写锁定;
- 同时可以有任意多个 gorouinte 获得读锁定;
- 同时只能存在写锁定或读锁定(读和写互斥)。
package main import ( "fmt" "math/rand" "sync" ) var count int var rw sync.RWMutex func main() { ch := make(chan struct{}, 10) for i := 0; i < 5; i++ { go read(i, ch) } for i := 0; i < 5; i++ { go write(i, ch) } for i := 0; i < 10; i++ { <-ch } } func read(n int, ch chan struct{}) { rw.RLock() fmt.Printf("goroutine %d 进入读操作...\n", n) v := count fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v) rw.RUnlock() ch <- struct{}{} } func write(n int, ch chan struct{}) { rw.Lock() fmt.Printf("goroutine %d 进入写操作...\n", n) v := rand.Intn(1000) count = v fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v) rw.Unlock() ch <- struct{}{} }
正则表达式
regexp包
Go语言通过 regexp 包为正则表达式提供了官方支持,其采用 RE2 语法,除了\c、\C外,Go语言和 Perl、Python 等语言的正则基本一致。
package main import ( "fmt" "regexp" ) func main() { buf := "abc azc a7c aac 888 a9c tac" //解析正则表达式,如果成功返回解释器 reg1 := regexp.MustCompile(`a.c`) if reg1 == nil { fmt.Println("regexp err") return } //根据规则提取关键信息 result1 := reg1.FindAllStringSubmatch(buf, -1) fmt.Println("result1 = ", result1) }
时间和日期
time 包提供了时间显示和测量等所用的函数
time
时间的获取
获取当前时间
package main import ( "fmt" "time" ) func main() { now := time.Now() //获取当前时间 fmt.Printf("current time:%v\n", now) year := now.Year() //年 month := now.Month() //月 day := now.Day() //日 hour := now.Hour() //小时 minute := now.Minute() //分钟 second := now.Second() //秒 fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second) }
获取时间戳
时间戳是自 1970 年 1 月 1 日(08:00:00GMT)至当前时间的总毫秒数,它也被称为 Unix 时间戳(UnixTimestamp)
package main import ( "fmt" "time" ) func main() { now := time.Now() //获取当前时间 timestamp1 := now.Unix() //时间戳 timestamp2 := now.UnixNano() //纳秒时间戳 fmt.Printf("现在的时间戳:%v\n", timestamp1) fmt.Printf("现在的纳秒时间戳:%v\n", timestamp2) }
时间戳转为时间格式
package main import ( "fmt" "time" ) func main() { now := time.Now() //获取当前时间 timestamp := now.Unix() //时间戳 timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式 fmt.Println(timeObj) year := timeObj.Year() //年 month := timeObj.Month() //月 day := timeObj.Day() //日 hour := timeObj.Hour() //小时 minute := timeObj.Minute() //分钟 second := timeObj.Second() //秒 fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second) }
获取当前是星期几
package main import ( "fmt" "time" ) func main() { //时间戳 t := time.Now() fmt.Println(t.Weekday().String()) }
时间操作函数
Add
Add 函数可以返回时间点 t + 时间间隔 d 的值
package main import ( "fmt" "time" ) func main() { now := time.Now() later := now.Add(time.Hour) // 当前时间加1小时后的时间 fmt.Println(later) }
Sub
两个时间之间的差值
Equal
两个时间是否相同
Before
一个时间点是否在另一个时间点之前
After
一个时间点是否在另一个时间点之后
定时器
使用 time.Tick(时间间隔) 可以设置定时器,定时器的本质上是一个通道(channel)
package main import ( "fmt" "time" ) func main() { ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器 for i := range ticker { fmt.Println(i) //每秒都会执行的任务 } }
时间格式化
时间类型有一个自带的 Format 方法进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S 而是使用Go语言的诞生时间 2006 年 1 月 2 号 15 点 04 分 05 秒。
package main import ( "fmt" "time" ) func main() { now := time.Now() // 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan // 24小时制 fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan")) // 12小时制 fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan")) fmt.Println(now.Format("2006/01/02 15:04")) fmt.Println(now.Format("15:04 2006/01/02")) fmt.Println(now.Format("2006/01/02")) }
解析字符串格式的时间
Parse 函数可以解析一个格式化的时间字符串并返回它代表的时间。
func Parse(layout, value string) (Time, error)
与 Parse 函数类似的还有 ParseInLocation 函数。
func ParseInLocation(layout, value string, loc *Location) (Time, error)
ParseInLocation 与 Parse 函数类似,但有两个重要的不同之处:
- 第一,当缺少时区信息时,Parse 将时间解释为 UTC 时间,而 ParseInLocation 将返回值的 Location 设置为 loc;
- 第二,当时间字符串提供了时区偏移量信息时,Parse 会尝试去匹配本地时区,而 ParseInLocation 会去匹配 loc。
OS
os 包中提供了操作系统函数的接口,是一个比较重要的包。顾名思义,os 包的作用主要是在服务器上进行系统的基本操作,如文件操作、目录操作、执行命令、信号与中断、进程、系统状态等等。
在 os 包下,有 exec,signal,user 三个子包
os/exec 执行外部命令
os/user 获取当前用户信息
os/signal 信号处理
命令行参数解析
在Go语言中的 flag 包中,提供了命令行参数解析的功能。
mod包依赖管理工具
go module 是Go语言从 1.11 版本之后官方推出的版本管理工具,并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具。
Modules 官方定义为:
Modules 是相关 Go 包的集合,是源代码交换和版本控制的单元。Go语言命令直接支持使用 Modules,包括记录和解析对其他模块的依赖性,Modules 替换旧的基于 GOPATH 的方法,来指定使用哪些源文件。
文件处理
JSON 文件
文本文件
写纯文本文件
package main import ( "bufio" "fmt" "os" ) func main() { //创建一个新文件,写入内容 filePath := "./output.txt" file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666) if err != nil { fmt.Printf("打开文件错误= %v \n", err) return } //及时关闭 defer file.Close() //写入内容 str := "http://c.biancheng.net/golang/\n" // \n\r表示换行 txt文件要看到换行效果要用 \r\n //写入时,使用带缓存的 *Writer writer := bufio.NewWriter(file) for i := 0; i < 3; i++ { writer.WriteString(str) } //因为 writer 是带缓存的,因此在调用 WriterString 方法时,内容是先写入缓存的 //所以要调用 flush方法,将缓存的数据真正写入到文件中。 writer.Flush() }
读纯文本文件
package main import ( "bufio" "fmt" "io" "os" ) func main() { //打开文件 file, err := os.Open("./output.txt") if err != nil { fmt.Println("文件打开失败 = ", err) } //及时关闭 file 句柄,否则会有内存泄漏 defer file.Close() //创建一个 *Reader , 是带缓冲的 reader := bufio.NewReader(file) for { str, err := reader.ReadString('\n') //读到一个换行就结束 if err == io.EOF { //io.EOF 表示文件的末尾 break } fmt.Print(str) } fmt.Println("文件读取结束...") }
数据库操作
安装依赖文件
go get github.com/go-sql-driver/mysql go get github.com/jmoiron/sqlx
链接mysql
package main import ( "fmt" _ "github.com/go-sql-driver/mysql" "github.com/jmoiron/sqlx" ) func connectMysql() (*sqlx.DB) { DB, err := sqlx.Open("mysql", "root:admin001@tcp(127.0.0.1:3306)/aliyun") if err != nil { fmt.Printf("mysql connect failed, detail is [%v]", err.Error()) } return DB } func queryData(Db * sqlx.DB) { rows, err := Db.Query("select * from tb_book") if err != nil { fmt.Printf("data insert faied, error:[%v]", err.Error()) return } for rows.Next(){ var id int var title string var created_at string var updated_at string err = rows.Scan(&id,&title,&created_at,&updated_at) if err != nil { fmt.Println(err.Error()) } fmt.Println(id,title,created_at,updated_at) } } func addRecord(Db *sqlx.DB) { for i:=0; i<2; i++ { result, err := Db.Exec("insert into userinfo values(?,?,?,?,?,?)",0, "2019-07-06 11:45:20", "johny", "123456", "技术部", "123456@163.com") if err != nil { fmt.Printf("data insert faied, error:[%v]", err.Error()) return } id, _ := result.LastInsertId() fmt.Printf("insert success, last id:[%d]\n", id) } } func updateRecord(Db *sqlx.DB){ //更新uid=1的username result, err := Db.Exec("update userinfo set username = 'anson' where uid = 1") if err != nil { fmt.Printf("update faied, error:[%v]", err.Error()) return } num, _ := result.RowsAffected() fmt.Printf("update success, affected rows:[%d]\n", num) } func deleteRecord(Db *sqlx.DB){ //删除uid=2的数据 result, err := Db.Exec("delete from userinfo where uid = 2") if err != nil { fmt.Printf("delete faied, error:[%v]", err.Error()) return } num, _ := result.RowsAffected() fmt.Printf("delete success, affected rows:[%d]\n", num) } func main() { var Db *sqlx.DB = connectMysql() defer Db.Close() queryData(Db) //addRecord(Db) //updateRecord(Db) //deleteRecord(Db) }
上面的示例中出现的问题怎么解决呢?加一个互斥锁 Mutex 就可以了