菜鸟教程 -- go教程 --读后梳理总结
历时近两个月,初次正儿八经的学习一门语言,把菜鸟教程上的go语言教程都精读了一遍,虽然很基础,但是自己看下来也并不轻松,写下这篇文章的目的是为了:
- 复习、总结、回顾
- 熟悉md格式的写法
- 梳理出疑问点,带着疑问去精读下一本教程
- 分享,如果真有人看,能一起探讨下就更好了
以下具体内容,主要分三部分来写
一、梳理、总结菜鸟教程的具体知识点
二、疑问点的具体总结
三、个人未来短期学习路线的明确,以及上季度打卡的总结
一、菜鸟教程
感觉菜鸟教程就是简单的将go语言的各知识点做一些简单的介绍,罗列,并没有串联起来,具体细节也不是很全,当然对于我这种菜鸟来说的话,感觉也还是挺不错的,在这里也给菜鸟的维护人员(据我所知好像就创始人一个人?)点赞以示感谢
1.环境安装、开发工具
- 环境安装
直接上官网上下载就好了,然后按照教程走就好了
- 开发工具
个人经历了用sublime写了(主要是教程中的简单的例题)之后,然后直接在终端跑;然后再来改这个文件,再保存再改;
以及用vscode来跑。
- 小结
感觉这个过程,对于像我这样的一张白纸来说,如果全程自己来整的话真的会有点小折磨,很浪费时间,也会很打击斗志,如果可以的话,在不让人厌烦的情况下,多问问过来人或是让帮忙装一下会好很多。
2.结构
9例:
package main //1.声明包,package main表示一个可独立执行的程序 import "fmt" //2.定义包,告诉 Go 编译器这个程序需要使用 fmt 包(实现输入、输出的一个包) func main(){ //3.执行main函数 fmt.Println("Hello, World!") //4.语句表达式 }
大致的结构如上,需要注意的是:
1. fmt.Println中的P,当大写的时候才会对外包(fmt)是可见的,这被称为**导出** ,如果是小写,则对包外是不可见的(执行起来会报错),但是他们在整个包的内部是可见并且可用的. 2. "{"大括号时,不能单独起一行 3. 注释可以用// 或是/**/
3.基础语法
- 标记
Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。 - 行分隔符
直接写多行就好了
例:
fmt.Println(xxx)//Prinln就表示的换行 fmt.Println(xxxx)
- 注释
常用的就是//和/**/ - 标识符
用来命名变量、程序实体;第一个字符只能是字母或下划线不能是数字且不能使关键字和运算符开头 - 字符串连接
用‘+’来实现 - 关键字
- 空格
例var age int
以及:在变量和运算符间加空格,看起来更舒服 - 格式化字符串
使用fmt.Printf 将需要输出的内容以指定格式来输出
4.数据类型
- 布尔型
true 、 false - 数字类型
- int 整数(表有符号)
- unit整型(无符号)
- float32 32位浮点型数
- float64 64位浮点型数
- Complex64 32 位实数和虚数
- Complex128 64位实数和虚数
- 字符串
string使用 UTF-8 编码标识 Unicode 文本 - 派生类别
常用的到的
- 指针类型ptr
- 数字类型
- 结构类型struct
- 通道类型 chan、ch
- 函数类型
- 切片类型(类数组)
- 接口类型(interface)
- map类型(集合)
- 其他
+ byte 类似uint8
+ rune类似int32
5.变量
- 全局变量
通常写在func main之前(函数体之前),通常用var的形式来写
例:
``` package main import "fmt" var x, y int = 1, 2 func main(){ fmt.Println(x,y) }```
- 局部变量
通常写在函数里面,用不带声明格式的方式来写
例:
package main import"fmt" func main(){ x, y := 1, 2 fmt.Println(x, y) }```
6.常量
关键字为const
- 显式类型定义 const b string = "nihao"
- 隐式类型定义 const a = "nihao"
7.运算符
- 算数运算符
+、-、*、\、++ 自增 - 关系运算符
主要就是 == ,判断是否相等的时候,为== 而不是= - 逻辑运算符
- && and
- || or
- !not
- 位运算符
需要用到的时候再实际去搜下看把 - 赋值运算符
主要就是 +=
例: C += A 等于 C = C + A - 涉及到指针的运算符
个人初理解是:
- &值 为返回地址
- *地址 为返回值
- 算数符优先级
8.条件语句
- if语句
- if...else语句
- if嵌套语句
- switch语句
- select语句(用于channel的相关操作)
9.循环语句
- for循环
- for{} 无限循环
- for 条件语句{}
- for i := 0; i < x; i++ {} //最常用的
- for key, value := range 表达式/变量{}
- range
个人理解就是遍历时常用到
10.函数
- 函数定义
func function_name( [parameter list] ) [return_types] {
函数体
}
需要注意的是:
- 参数列表需要用()括起来,而返回值类型不用
- 可以没有返回值(有些功能不需要返回值)
- 函数体:函数定义的代码集合
- 返回多个值
package main import "fmt" func swap(x int, y string) (string, int) { return y, x } func main() { a, b := swap(123, "Google") fmt.Println(a, b) }
- 方法
func (variable_name variable_data_type) function_name() [return_type]{ /* 函数体*/ }
参照原本的例题,自己改写了一下后:
package main import ( "fmt" ) /* 定义结构体 */ type Circle struct { radius float64 id float64 } //该 method 属于 Circle 类型对象中的方法 func (c Circle) getArea() float64 { //c.radius 即为 Circle 类型对象中的属性 return 3.14 * c.radius * c.id } func main() { var c1 Circle c1.radius = 10.00 c1.id = 2.00 fmt.Println("圆的面积 = ", c1.getArea()) }
- 以上代码,只是单纯为了测试当结构体有两个值时也是行得通的。
- 那么个人感觉方法的定义就像是:
先定义了结构体之后,再通过像是写函数的方式一样,往这个结构体加功能即方法。调用的时候,只用调用这个结构体内的这个方法就好了。
11.变量作用域
- 函数内定义的变量称为局部变量(函数开始之前定义的?)
package main import "fmt" func main() { /* 声明局部变量 */ var a, b, c int /* 初始化参数 */ a = 10 b = 20 c = a + b fmt.Printf ("结果: a = %d, b = %d and c = %d\n", a, b, c) }
- 函数外定义的变量称为全局变量
package main import "fmt" /* 声明全局变量 */ var g int = 20 func main() { /* 声明局部变量 */ var g int = 10 fmt.Printf ("结果: g = %d\n", g) }
- 函数定义中的变量称为形式参数(形式参数会作为函数的局部变量来使用)
-- 以下这个例子还挺方便理解的
package main import "fmt" /* 声明全局变量 */ var a int = 20; func main() { /* main 函数中声明局部变量 */ var a int = 10 var b int = 20 var c int = 0 fmt.Printf("main()函数中 a = %d\n", a); c = sum( a, b); fmt.Printf("main()函数中 c = %d\n", c); } /* 函数定义-两数相加 */ func sum(a, b int) int { fmt.Printf("sum() 函数中 a = %d\n", a); fmt.Printf("sum() 函数中 b = %d\n", b); return a + b; }
12.数组
- 数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。
- 通过索引(即key或是叫位置)来读取或是修改,从0开始,第二个索引为1
- 感觉切片和数组好像,并且用的时候也感觉很相似,切片就直接放到下面了
- 声明数组
var variable_name [SIZE] variable_type//var 数组名[数组大小] 数据类型
例:var balance [10] float32
- 初始化数组
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
或是balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
- 还可以通过指定下标来初始化元素:
balance := [5]float32{1:2.0,3:7.0}
- 如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:
- 访问数组元素
package main import "fmt" func main(){ var shuzu[5] int for i := 0; i < 5; i++ { shuzu[i] = 100 + i fmt.Printf("shuzi[%d]:=%d\n", i, shuzu[i]) } } //仿照原本的例子,自己稍微改写的
13.切片slice
- 切片是数组的一个引用,因此是引用类型
- 遍历切片,访问切片的元素和求切片长度len(slice)都一样
- 长度是可变的
- 切片定义的基本语法: var 切片名 [] 类型 比如:var a[] int
- 参考网站https://www.csdn.net/tags/NtzaEg0sMTY1MTUtYmxvZwO0O0OO0O0O.html#13_18
- 基本写法
package main import ( "fmt" ) func main(){ intArr := [5]int{1,22,33,66,99} slice :=intArr[1:3]//定义一个切片,slice就是切片名称 //arr[1,3]表示slice引用intArr这个数组,引用arr数组起始下标为1至下标为3(不包含3) fmt.Println("intArr=:" ,intArr)//intArr=: [1 22 33 66 99] fmt.Println("slice 的内容" , slice)//slice 的内容 [22 33] fmt.Println("slice 长度为" , len(slice))//slice 长度为 2 fmt.Println("slice 的容量为" ,cap(slice))// slice 的容量为 4 }
- 底层含义
- slice是一个引用类型
- slice从底层来说是一个数据结构(struct结构体)
type slice struct{ ptr *[2]int //这里还是不怎么理解? len int cap int } //所以这么看就好理解len()和cap()是这里面的内置函数了?
- make()
通过make来创建切片
基本语法:var 切片名 [] type = make([]type,len,[cap])
参数说明:type:数据类型 len:大小 cap:指定切片容量,可选,如果分配cap,则cap>=len
例:
package main import ( "fmt" ) func main(){ var slice2 []float64 =make([]float64,5,10) slice2[0]=10 slice2[3]=20 fmt.Println("slice2 的内容" , slice2)//slice2 的内容 [10 0 0 20 0] fmt.Println("slice2 长度为" , len(slice2))//slice2 长度为 5 fmt.Println("slice2 的容量为" ,cap(slice2))//slice2 的容量为 10 }
- 通过make方式可以创建切片可以指定切片的大小和容量
- 如果没有给切片的个元素赋值,则为默认值
- 通过make方式创建的切片对应的数组是由make底层维护的,对外不可见,只能通过slice去访问各个元素
- 直接定义
- 原理类似make
package main import ( "fmt" ) func main(){ var slice3 []string =[]string{"tom","jack","mary"} fmt.Println("slice3 的内容" , slice3)//slice3 的内容 [tom jack mary] fmt.Println("slice3 长度为" , len(slice3))//slice3 长度为 3 fmt.Println("slice3 的容量为" ,cap(slice3))//slice3 的容量为 3 }
- 以上定义(声明)切片时的区别
- 直接引用数组的时候,是事先存在的,程序员可见的
- 用make来创建的时候,是由切片在底层维护,程序员不可见
- append函数
用append内置函数,可以对切片进行动态追加
package main import ( "fmt" ) func main(){ var slice3 []string =[]string{"tom","jack","mary"} fmt.Println("slice3 的内容" , slice3)//slice3 的内容 [tom jack mary] fmt.Println("slice3 长度为" , len(slice3))//slice3 长度为 3 fmt.Println("slice3 的容量为" ,cap(slice3))//slice3 的容量为 3 }
- string和slice
- string的底层就是一个byte数组,所以string也可以进行切片处理
str := "wang" slice:= str[0:2] fmt.Println(slice)//wa
- string是不可变的,不能通过str[0]="a"来修改字符串
- 如果需要修改,可以先将stirng转为[]byte 或[]rune修改完转换为string
str := "wang" slice:= str[0:2] fmt.Println(slice)//wa arr1 := []byte(str) arr1[0] = 'a' str=string(arr1) fmt.Println(str)//aang
如果包含汉字转化为rune
str := "wang" slice:= str[0:2] fmt.Println(slice)//wa arr1 := []rune(str) arr1[0] = '王' str=string(arr1) fmt.Println(str)//王ang
- 二维数组
- 例1:
var arr [2][3]int arr[0][2]=1 arr[1][1]=2 for i:=0;i<2;i++{ for j:=0;j<3;j++{ fmt.Print(arr[i][j]," ") } fmt.Println() } //0 0 1 //0 2 0
例2:
package main import "fmt" func main() { /* 数组 - 5 行 2 列*/ var a = [5][2]int{ {0,0}, {1,2}, {2,4}, {3,6},{4,8}} var i, j int /* 输出数组元素 */ for i = 0; i < 5; i++ { for j = 0; j < 2; j++ { fmt.Printf("a[%d][%d] = %d\n", i,j, a[i][j] ) } } }
14.指针
- 指针定义
- 一个指针变量指向一个值的地址
- 例
var ip *int
//此时ip就已经是一个地址了
- *和&的用法
- *一般用来返回地址的值
- &一般用来返回值的地址
- 需要注意的是
- 定义指针变量的时候的∗和具体函数体内用到的∗并不是同一个意思
例:
package main import "fmt" func swap(x *int, y *int) { //1.这里的x和y其实就是地址来的 var temp int temp = *x /* 保存 x 地址的值 */ //所以这里要用*x来传递 *x = *y /* 将 y 赋值给 x */ //因为temp是int,值来的 *y = temp /* 将 temp 赋值给 y */ } 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); //2.所以这里需要&a和&b fmt.Printf("交换后 a 的值 : %d\n", a ) fmt.Printf("交换后 b 的值 : %d\n", b ) }
15.结构体
- 定义:
- 结构体就是可以由各种不同数据类型的数据组成的数据类型
- 定义结构体需要用到type和struct语句
例:
package main import "fmt" type Books struct { title string author string subject string book_id int } func main() { // 创建一个新的结构体 fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407}) // 也可以使用 key => value 格式 fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407}) // 忽略的字段为 0 或 空 fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"}) }
- 赋值及访问(打印)
都是使用.符号来操作
例:
Book1.title = "Go 语言" 以及 fmt.Printf( "Book 2 title : %s\n", Book2.title)
- 结构体参数
结构体也可以当作参数一样,直接传递给函数 - 结构体指针
例子没看太懂
16.范围range
- 用来遍历数组(array)、切片(slice)、通道(channel)、集合(map)中的index和value或是key和value
- 通常搭配for一起用:for key, value := range xx{}
例:
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) } }
17.集合(map)
- Map是无序的键值对,通过 key 来快速检索数据,key 类似于索引,指向数据的值。
- Map是一种集合,无法决定它返回的顺序,因为其是用hash表来实现的。
- 定义map
- 可以用map关键字
例:var map_name map[key_type]value_type
- 也可以用make函数(有点类似切片了)
例:map_name := make(map[key_type]value_type)
实例:
package main import "fmt" func main() { var countryCapitalMap map[string]string /*创建集合 */ countryCapitalMap = make(map[string]string) /* map插入key - value对,各个国家对应的首都 */ countryCapitalMap [ "France" ] = "巴黎" countryCapitalMap [ "Italy" ] = "罗马" countryCapitalMap [ "Japan" ] = "东京" countryCapitalMap [ "India " ] = "新德里" /*使用键输出地图值 */ for country := range countryCapitalMap { fmt.Println(country, "首都是", countryCapitalMap [country]) } /*查看元素在集合中是否存在 */ capital, ok := countryCapitalMap [ "American" ] /*如果确定是真实的,则存在,否则不存在 */ /*fmt.Println(capital) */ /*fmt.Println(ok) */ if (ok) { fmt.Println("American 的首都是", capital) } else { fmt.Println("American 的首都不存在") } }
- delete函数
- 用于删除集合的元素, 参数为 map 和其对应的 key。
例:delete(countryCapitalMap, "France")
18.接口(interface)
- 接口也是一种数据类型
- 它把所有具有共性的方法定义在了一起
- 任何其他类型只要实现了这个方法就是实现了这个接口
例:
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() }
- 在上面的例子中,我们定义了一个接口Phone,接口里面有一个方法call()。然后我们在main函数里面定义了一个Phone类型变量,并分别为之赋值为NokiaPhone和IPhone。然后调用call()方法
19.类型转换
- 格式
type_name(expression)
- type_name 为类型,expression 为表达式。
- 例
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) }
20.递归函数
- 定义
- 就是在运行过程中调用自己
- 语法格式如下
func recursion() { recursion() /* 函数调用自身 */ } func main() { recursion() }
- 使用递归时,需要设置退出条件,否则会陷入无限循环
2.阶乘的例子
package main import "fmt" //其实这里的返回值的时候也可以只写uint64,但是这样的话函数体内就要再声明一下result的数据类型了,因为会用到 func Factorial(n uint64)(result uint64) { if (n > 0) { result = n * Factorial(n-1) return result } return 1 } func main() { var i int = 15 fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i))) }
- 斐波那契数列
package main import "fmt" func fibonacci(n int) int { if n < 2 { return n } return fibonacci(n-2) + fibonacci(n-1) }//为什么要写成这样,直接就(n-2) + (n-1)不行吗 // -- 因为不调用自己就没办法循环,也就没办法实现效果 func main() { var i int for i = 0; i < 10; i++ { fmt.Printf("%d\t", fibonacci(i)) } } //感觉用python的写法来实现这个数列的时候会更简洁一些?
21.错误处理
看了好多遍,越看越乱,看不懂,感觉很多点没说道,就直接跳跃性的出现了,下一本书再重新捋一下
22.并发与通道
- 并发
- 通过 go 关键字来开启 goroutine
go 函数名( 参数列表 )
- 例
package main import ( "fmt" "time" ) func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("world") say("hello") }
- 通道(channel)
- 用来传递数据的数据结构
- ch <- v // 把 v 发送到通道 ch
- v := <-ch // 从 ch 接收数据// 并把值赋给 v
- 创建(声明)通道: ch := make(chan int)
例:
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // 把 sum 发送到通道 c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // 从通道 c 中接收 fmt.Println(x, y, x+y) }
- 通道缓冲区
- make 的第二个参数指定缓冲区大小:
- ch := make(chan int, 100) //这里的大小是指可以存放的数量
例:
package main import "fmt" func main(){ //这里我们定义一个可以存储整数类型的缓冲通道 //缓冲区大小为2 ch := make(chan int, 2) //因为ch是带缓冲通道的,我们可以同时发送两个数据 //而不用立刻需要去同步读取数据 ch <- 1000 ch <- 2000 //获取这两个数据 fmt.Println(<-ch) fmt.Println(<-ch) }
- 遍历通道与关闭通道
package main import ( "fmt" ) func fibonacci(n int, c chan int) { x, y := 0, 1 for i := 0; i < n; i++ { c <- x x, y = y, x+y } close(c) } func main() { c := make(chan int, 10) go fibonacci(cap(c), c) // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个 // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据 // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不 // 会结束,从而在接收第 11 个数据的时候就阻塞了。 for i := range c { fmt.Println(i) } }
二、看下一本书需要更加留意的点
- 指针:* & 的用法,在用的时候还是会经常搞混
- 函数:概念还是不是很熟悉,包括参数是切片、指针的时候,就更容易搞不清了,需要多做点题?
- 循环
- 条件语句
- 通道
- 方法
- 切片:已经有长度这个概念了,为什么要引入容量这个概念,默认是怎样的,怎么扩充,为什么需要扩充
- 集合
- 接口
- 错误处理
细一想,感觉好多概念,有点印象,又不是很清楚的样子,这么看的话,大部分知识点,感觉掌握的并不是很好