golang基础学习
一、 前言
Go
编程语言是一个开源项目,它使程序员更具生产力。Go
语言具有很强的表达能力,它简洁、清晰而高效。得益于其并发机制,用它编写的程序能够非常有效地利用多核与联网的计算机,其新颖的类型系统则使程序结构变得灵活而模块化。Go
代码编译成机器码不仅非常迅速,还具有方便的垃圾收集机制和强大的运行时反射机制,它是一个快速的、静态类型的编译型语言,感觉却像动态类型的解释型语言。
第一个go程序
1 | package main |
解释:如果是为了将代码编译成一个可执行程序,那么package必须时main
如果是为了将代码编译成库,那么pacakge
则没有限制Go
中所有的代码都应该隶属一个包。
fmt
是go
的一个系统库fmt.Println()
则可以打印输出,如果想要运行程序,执行命令go run 程序名
在一个可执行程序中只能只有一个main
函数。
go的结构开发规范
好的代码规范非常重要,这样当你看别人的代码或者别人看你的代码的时候就能很清楚的明白,下面是go
程序基本的结构规范:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18//当前程序的包名
package main
//导入其他的包
import "fmt"
//常量的定义
const PI =3.14
//全局变量的声明和赋值
var name = "gopher"
//一般类型声明
type newType int
//结构的声明
type gopher struct{}
//接口的声明
type golang interface{}
//函数
func funcName(a type1, b type2) (c type1, d type2) {
return c, d
}
二、基础语法
2.1 变量
函数内声明的为局部变量,作用域为函数体,函数外声明的变量为全局变量,作用域为包。1
2
3
4
5
6
7
8
9
10
11
12
13// 声明:
var a int // 声明 int 类型的变量
var b [10] int // 声明 int 类型数组
var c []int // 声明 int 类型的切片
var d *int // 声明 int 类型的指针
// 赋值:
a = 10
b[0] = 10
// 同时声明与赋值
var a = 10
//等价于
a := 10
a,b,c,d := 1,2,true,"goodboy"
2.2 常量
1 | const filename = "ab" |
2.3 条件语句与循环
if
1
2
3
4
5
6
7
8
9
10
11if a > 100 {
return 100
}else if a >50 {
return 50
}else{
return 0
}
if a,b := 1,2; a+b>3{
fmt.Println(a,b)
}
fmt.Println(a,b) // 错误! a,b的是 if 条件里定义的,作用域仅限于 if 中使用
利用if语句判断读取文件时是否有异常
1 | import ( |
switch
go中的switch不需要手动break1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20var grade string = "B"
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 );
for
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 赋值语句;判断语句;递增语句
for i:=100; i>0; i--{
fmt.Println(i)
}
// 无赋值
func test(n int){
for ; n>0 ; n/=2 {
fmt.Println(n);
}
}
// 仅赋值
scanner := bufio.NewScanner(file)
for scanner.Scan(){
fmt.Println(scanner.Text);
}
// 死循环
for{
fmt.Println(1);
}
for range
语句1
2
3
4
5str := "hello 世界"
for i,v := range str {
fmt.Println("index[%d] val[%c] len[%d]\n",i,v,len([]byte(string(v))))
}
//这里需要注意一个问题,range str 返回的是两个值,一个是字符串的下标,一个是字符串中单个字符
label
1
2
3
4
5
6
7
8
9
10
11
12
13
14package main
import "fmt"
func main() {
LABEL1:for i:=0;i<5;i++{
for j:=0;j<5;j++{
if j == 4{
continue LABEL1
}
fmt.Printf("i is :%d and j is:%d\n",i,j)
}
}
}
代码中我们在continue 后面添加了一个LABEL1这样当循环匹配到j等于4的时候,就会跳出循环,重新回到最外成i的循环,而如果没有LABEL1则就会跳出j的本次循环,执行j++进入到j的下次循环
2.4 函数
2.4.1 函数声明
1 | //语法:func 函数名(参数列表)(返回列表){} |
2.4.2 go
函数的特点
- 不支持重载,一个包不能包含两个名字一样的函数
- 函数是一等公民,函数也是一种类型,一个函数可以赋值给变量
- 匿名函数
- 多返回值
下面通过例子来演示第二个特点1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21package main
import (
"fmt"
)
type op_func func(int,int) int
func add(a,b int) int {
return a + b
}
func operator(op op_func,a,b int) int{
return op(a,b)
}
func main() {
c := add
sum := operator(c,100,200)
fmt.Println(sum)
}
2.4.3 可变参数
表示0个或多个参数1
func add(arg ...int) int {}
表示1个或多个参数
func add(a int,arg ...int) int {}
其中arg是一个slice,我们可以通过arg[index]
获取参数,通过len(arg)
可以判断参数的个数
2.5 指针
go语言的参数传递是值传递,无论是值传递还是引用传递,传递给函数的都是变量的副本,不过值传递的是值的拷贝,引用传递是传递的地址的拷贝,一般来说,地址拷贝更为高效,而值拷贝取决于拷贝的对象的大小,对象越大,则性能越低
1 | func main() { |
普通的类型,变量存的就是值,也叫值类型 获取变量的地址,用&, 指针类型,变量存的是一个地址,这个地址存的才是真正的值 获取指针类型所指向的值,用,例如:var p int, 使用 *p获取p指向值 通过下面的代码例子理解:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17package main
import "fmt"
func main() {
var a int = 10
//通过&a打印a的指针地址
fmt.Println(&a)
//定义一个指针类型的变量p
var p *int
//讲a的指针地址复制给p
p = &a
fmt.Println(*p)
//给指针p赋值
*p = 100
fmt.Println(a)
}
三、内建容器
3.1 数组
定义数组
1
2
3
4
5
6
7
8var arr [3]int // 会初始化为 [0,0,0]
arr := [3]{1,2,3}
arr := [...]{1,2,3,4,5}
arr := [2][4]int // 2行4列
b := [2]string{"Penn", "Teller"}
//等价
b := [...]string{"Penn", "Teller"}
遍历数组
1
2
3
4arr := [3]{1,2,3}
for k,v := range(arr){
fmt.Println(k, v) // k为索引,v为值,0,1 1,2 2,3
}
数组作为函数参数
按值传递,,注意,[5]int
与 [10]int
是不同的类型 ,go
语言一般不直接使用数组,而是使用切片。1
2
3
4
5
6
7
8
9func printArr(arr [5]int) {
for k,v:=range (arr){
fmt.Println(k,v)
}
}
func main() {
arr :=[5] int {6,7,8,9,10}
printArr(arr)
}
3.2 切片
Go
的切片类型为处理同类型数据序列提供一个方便而高效的方式。 切片有些类似于其他语言中的数组,但是有一些不同寻常的特性。类似于python
中的list
用法。 本节将深入切片的本质,并讲解它的用法。
切片是数组的“视图”,即引用
1 | // 前开后闭 |
直接创建切片1
2
3s := []int{1,2,3}
var s []int // 会初始化为 nil
s := make([]int, 16, 32) // make(切片类型,切片长度,切片cap长度)
添加元素1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28// 若添加元素个数不超过cap值,则对原数组进行修改
arr :=[5] int {0,1,2,3,4}
arr1 := arr[1:3]
arr2 := append(arr1, 10, 11)
fmt.Println(arr1,arr2,arr) // [1 2] [1 2 10 11] [0 1 2 10 11]
// 若添加元素个数超过cap值,则开辟新数组,拷贝并添加
arr :=[5] int {0,1,2,3,4}
arr1 := arr[1:3]
arr2 := append(arr1, 10, 11, 12)
fmt.Println(arr1,arr2,arr) // [1 2] [1 2 10 11 12] [0 1 2 3 4]
func main() {
var s []int
for i:=0; i<10; i++ {
s = append(s,i)
fmt.Println(s, cap(s))
}
}
结果:(当cap超出,就会重新分配cap值更大的新数组)
[0] 1
[0 1] 2
[0 1 2] 4
[0 1 2 3] 4
[0 1 2 3 4] 8
[0 1 2 3 4 5] 8
[0 1 2 3 4 5 6] 8
[0 1 2 3 4 5 6 7] 8
[0 1 2 3 4 5 6 7 8] 16
[0 1 2 3 4 5 6 7 8 9] 16
copy
1
2
3
4s1 := []int{0,1,2,3}
s2 := make([]int, 6)
copy(s2,s1)
fmt.Println(s1,s2) // [0 1 2 3] [0 1 2 3 0 0]
3.3 Map
定义1
2
3
4
5
6
7
8m := map[string]int{} // nil
var m map[string]string // nil
m := make(map[string]string) // empty map
m2 := map[string]string{
"name":"zy",
"age":"10",
}
fmt.Println(m2) // map[name:zy age:10]
遍历
`map`是无序的`hash map`,所以遍历时每次输出顺序不一样
1 | m := map[string]string{ |
取值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22m := map[string]string{
"name":"zy",
"age":"10",
}
name := m["name"]
fmt.Println(name) // “zy”
// 获取一个不存在的值
value := m["aaa"]
fmt.Println(value) // “” 返回一个空值
// 判断key是否存在
value, ok := m["aaa"]
fmt.Println(value, ok) // "" false
// 标准用法:
if v,ok:= m["name"]; ok{
fmt.Println(v)
}else{
fmt.Println("key not exist!")
}
//del
delete(m, "name")
四、面向对象
go语言的面向对象仅支持封装,不支持继承和多态
4.1 结构体
定义1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 定义一个结构体
type treeNode struct {
value int
left,right *treeNode
}
func main() {
root := treeNode{1,nil,nil}
node1 := treeNode{value:3}
root.left = &node1
root.left.right = new(treeNode) // 内建函数初始化node
nodes := []treeNode{
{1,nil,nil},
{2,&root,&node1},
}
fmt.Println(nodes[1].left.left.value) // 3
}
自定义工厂函数
由于没有构造函数,所以可以用工厂函数代替
1 | func createNode(value int) *treeNode { |
结构体方法
结构体方法并不是写在结构体中,而是像函数一样写在外面,它实际上久是定义了[接收对象]的函数
由于本质依然是函数,所以也是按值传递,若要改变对象,用指针
1 | type treeNode struct { |
4.2 封装
名字一般用 CamelCase
首字母大写是 public 方法
首字母小写是 private 方法(2和3 对于变量常量也依然适用)
包
每个目录只有一个包 (package)
main包包含程序入口
为某结构体定义的方法必须放在同一个包内,但可以放不同文件
1 |
|
没有继承
go语言没有继承,如何扩展系统类型或者自定义类型呢?
1. 定义别名
2. 使用组合
4.3 接口
接口定义1
2
3type xxx interface{
FuncName() string // 定义接口方法与返回类型
}
实现
结构体不需要显示“实现”接口,只要定义好接口方法即可
1 | // interface/interface.go |
类型
查看类型: i.(type)
1 | var i AnimalInterface // 定义变量 i 是动物接口类型 |
约束接口类型:i.(xxx)
1 | var i AnimalInterface // 定义变量 i 是动物接口类型 |
泛型: interface{}
1 | type Queue []int // 定义了一个 int 类型的切片 |
组合1
2
3
4
5
6
7
8
9
10
11
12
13
14type Cat interface{
cat()
}
type Dog interface{
dog()
}
type Animal interface{ // 要实例既实现 Cat 又实现 Dog
Cat
Dog
}
func get(i Animal){
i.cat()
i.dog()
}
常用系统接口1
2
3
4
5
6
7
8
9
10
11
12// 1. 类似 toString() 的信息打印接口
type Stringer interface{
String() string
}
// 2. Reader
type Reader interface{
Read(p []byte) (n int, err error)
}
// 3. Writer
type Writer interface{
Write(p []byte) (n int, err error)
}
五、goroutine
并发
5.1 goroutine
`go func(){}()`
用 `go` 关键字开启协程,协程是非抢占式多任务处理,由协程主动交出控制权
1 | func main() { |
1 | func main() { |
goroutine可能交出控制权的点:
- I/O操作,select
- channel
- 等待锁
- 函数调用时(有时,不一定)
- runtime.Gosched()
5.2 channel
channel其实就是传统语言的阻塞消息队列,可以用来做不同goroutine之间的消息传递
定义
声明一个channel:
var c chan int
c := make(chan int)使用 channel:
c <- 1 // 向管道中写入
d := <-c // 从管道读出
1 | func worker(c chan int) { |
更进一步:channel工厂1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17func workerFactory(i int) chan int{
c := make(chan int)
go func(c chan int) {
fmt.Println( <-c )
}(c)
return c
}
func main() {
var arr [10] chan int
for i:=0; i<10 ; i++ {
arr[i] = workerFactory(i) // 创建 channel 并监听
}
for i:=0; i<10 ; i++ {
arr[i] <- i // 向各channel中输入
}
time.Sleep(time.Microsecond)
}
channel 方向(只读与只写)1
2
3
4
5
6
7
8
9
10func workerFactory() chan <- int{ // 返回的是只写channel
c := make(chan int)
go func(c chan int) {
fmt.Println( <-c )
}(c)
return c
}
c := workerFactory(0)
r := <- c // 报错!
// 同理, `<- chan int` 是只读channel
缓冲区
向管道中写入就必须定义相应的输出,否则会报错
有缓冲区与无缓冲区的区别是 一个是同步的 一个是非同步的,即阻塞型队列和非阻塞队列 详解:https://blog.csdn.net/samete/article/details/52751227
1 | c := make(chan int, 3) // 缓冲区长度3 |
关闭管道
当确定不再向缓冲区中发送数据,可以关闭管道,若还有协程在不断接收 管道数据,则会源源不断的收到 0 即空串,直到程序结束。
1 | func workerFactory() chan int{ |
用channel等待任务结束
上面的例子使用 time.Sleep(time.Microsecond)来等待任务结束,不精确且耗时1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42package main
import (
"fmt"
)
type worker struct {
in chan int // in 用来读写
done chan bool // done 用来表示已读取完成
}
func createWorker() worker{
worker := worker{
in:make(chan int),
done:make(chan bool),
}
doWorker(worker);
return worker
}
func doWorker(w worker) {
go func(w worker) {
for {
fmt.Println(<-w.in)
go func(w worker) { // 注意此处也要 go,不然阻塞
w.done<-true
}(w)
}
}(w)
}
func main() {
var arr [10] worker
for i:=0; i<10; i++ {
arr[i] = createWorker()
}
for i:=0; i<10; i++ {
arr[i].in<-i
}
for i:=0; i<10; i++ {
arr[i].in<-i
}
for i:=0; i<10; i++ {
<-arr[i].done
<-arr[i].done
}
}
用 sync.WaitGroup 等待任务结束1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41package main
import (
"fmt"
"sync"
)
type worker struct {
in chan int
wg *sync.WaitGroup // *
}
func createWorker(wg *sync.WaitGroup) worker{
worker := worker{
in:make(chan int),
wg:wg,
}
doWorker(worker);
return worker
}
func doWorker(w worker) {
go func(w worker) {
for {
fmt.Printf("%c \n", <-w.in)
w.wg.Done() // 发送任务结束的信号
}
}(w)
}
func main() {
var wg sync.WaitGroup // 定义WaitGroup
var arr [10] worker
for i:=0; i<10; i++ {
arr[i] = createWorker(&wg) //按址传递,用一个引用来开始和结束
}
for i:=0; i<10; i++ {
wg.Add(1) // 开始一个任务前,计时器加一(一定要在开始前加)
arr[i].in <- 'a'+i
}
for i:=0; i<10; i++ {
wg.Add(1)
arr[i].in <- 'A'+i
}
wg.Wait() // 阻塞等待所有任务 done
}
5.3 select
- 有多个 case 语句,只要有一个 case 处于非阻塞可执行状态,就执行,否则一直阻塞
- 如果有多个case都可以运行,select会随机公平地选出一个执行,其他不会执行
用法1
2
3
4
5
6
7
8
9
10
11func main() {
var c1,c2 chan int
select {
case n:=<-c1:
fmt.Println(n)
case n2:=<-c2:
fmt.Println(n2)
default:
fmt.Println("no value") // ✔️
}
}
1 | func create(i int) chan int{ |
定时器
- time.After() 设置一个定时器,返回一个 channel,到一段时间后,向channel发送一条当前时间
- time.Tick() 返回一个 channel,每过一段时间向channel发送一条当前时间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18func main() {
c1,c2 := create(1),create(2)
tm := time.After(2*time.Second) // 定时器,2秒后触发
tm2 := time.Tick(1*time.Second) // 每1秒触发一次
for {
select {
case n1 := <-c1:
fmt.Printf("%c \n",n1)
case n2 := <-c2:
fmt.Printf("%c \n",n2)
case t := <- tm2:
fmt.Println("------- ",t," -----------")
case <- tm:
fmt.Println("bye")
return
}
}
}
六、结束语
哎~终于完了,还是有很多没有弄明白,先总结在这里,方便以后查阅。