我把go的接口和并发放在了进阶篇,因为这是go的核心,不能浅显地去理解它地语法,需要深入。而基础篇注重一些语法地表达 go
1. GoLang的应用方向及基本特性
应用方向:
- 区块链开发工程师
- GO服务器端、游戏软件工程师
- golang分布式,云计算
1.1 learning way:now how to know why? 整体框架到细节
go语言设计者设计的初衷:
1.当前硬件发展很迅速,软件的编程没有跟上发展速度,不能发挥多核多CPU的优势
2.软件系统复杂度越来越高,维护成本很高不够,需要简洁高效的语言(现有的编程语言风格不统一,计算能力不够,处理大并发不够好)
3.企业运行和维护c、c++的项目虽然运行速度快,但是编译速度慢,而且还有内存泄漏的问题存在
1.2 go语言特点
既有静态编译语言的安全和性能,有达到动态语言开发维护的效率
1.弱化的指针
2.引入包的概念,每一个文件都要归属于一个包而不能单独存在
3.类似于java 的垃圾回收机制
4.天然并发,从语言层面支持并发,实现简单,goroutine,轻量级线程,可实现大并发处理,高效利用多核,基于CPS并发模型实现
5.吸收了管道通信机制
6.函数可以返回多个值
func getSumAndSub(n1 int,n2 int)(int,int){
sum:=n1+n2
sub:=n1-n2
return sum,sub
}
7.类似java语言的集合,有切片,延时执行defer等
8.与java不同的是,导入的包如果不使用编译不通过,在多人开发的时候,冗余的代码包和变量就会不参与编译,提高代码性能
1.3 go的执行流程
注意编译的时候,编译器会把执行程序的一些依赖文件都添加到可执行文件中,所以可执行文件会变大
package main
import "fmt"
func main(){
fmt.Println("tomcat")
}
2. Golang的类型
2.1注释
- 行注释://注释内容
- 块注释:/*跨行注释内容*/
2.2go语言的代码规范要求
1.尽量使用行注释
2.要有合适的缩进和空白,函数体的缩进,以及运算符左右两边都要添加空格
3.可以使用goformat指令修复格式 :gofmt -w 文件名(-w写入文件的操作)
4.花括号的位置不能调换
5.如果单行长度很长可以用,隔开
6.golang每行末会自行添加;字符串拼接和python等同+
2.3go语言的变量
变量的命名建议:
- 驼峰命名
- 局部变量优先使用短名
- 专有名词大写
首先理解go语言中的变量和内存中的关系
如果一个变量a=b就是把b的值赋值给a,而不是引用或者指针
但是使用&a=&b的时候,就是把b的地址赋给a的地址,这时候修改a的值的时候b的值就改变了
2.3.1变量类型
标准库math定义了个数字类型的取值范围
四大基本类型
| 序号 | 类型和描述 |
|---|---|
| 1 | 布尔型 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。 |
| 2 | 数字类型 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 |
| 3 | 字符串类型: 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 |
| 4 | 派生类型: 包括:(a) 指针类型(Pointer)(b) 数组类型(c) 结构化类型(struct)(d) Channel 类型(e) 函数类型(f) 切片类型(g) 接口类型(interface)(h) Map 类型 |
数字类型的分支
| 序号 | 类型和描述 |
|---|---|
| 1 | uint8 无符号 8 位整型 (0 到 255) |
| 2 | uint16 无符号 16 位整型 (0 到 65535) |
| 3 | uint32 无符号 32 位整型 (0 到 4294967295) |
| 4 | uint64 无符号 64 位整型 (0 到 18446744073709551615) |
| 5 | int8 有符号 8 位整型 (-128 到 127) |
| 6 | int16 有符号 16 位整型 (-32768 到 32767) |
| 7 | int32 有符号 32 位整型 (-2147483648 到 2147483647) |
| 8 | int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
| 序号 | 类型和描述 |
|---|---|
| 1 | float32 IEEE-754 32位浮点型数 |
| 2 | float64 IEEE-754 64位浮点型数 |
| 3 | complex64 32 位实数和虚数 |
| 4 | complex128 64 位实数和虚数 |
2.3.2变量赋值方式
变量代表一个存储区域,该区域有自己的名称(变量名)和类型(数据类型)
golang中三种变量的声明方式:
//第一种,指定变量类型,声明后不赋值,使用默认值
var myVal1 string
//第二种,根据数值自行判断变量类型(类型推导)
var myVal2 = happy
//第三种,省略var,注意:=左边的变量不应该是已经声明过的,否则会产生编译错误,:=又被称为初始化声明
myVal3 := 1
//第四种,多变量声明:会先计算等号右边的表达式,然后在一起赋值给左边的变量
// 类型相同多个变量, 非全局变量
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3
var vname1, vname2, vname3 = v1, v2, v3 // 和 python 很像,不需要显示声明类型,自动推断
vname1, vname2, vname3 := v1, v2, v3
// 这种因式分解关键字的写法一般用于声明全局变量
var (
vname1 v_type1
vname2 v_type2
)
//不同类型的初始化声明
a, b, c := 5, 7, "abc"
//变量初始化会被自动识别出类型,但是记住float64,而不是float32,go语言圣经中也建议我们尽量不要使用float32容易出错
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
//初始化声明也可以退化成赋值声明,在已经赋值过的变量的基础上对它在赋值,但是有一个限制,变量的作用域必须相同
package main
import (
"fmt"
)
func main(){
x:=100
fmt.println(x)
{
x:=200
fmt.println(x)
}
}
//这里的两个变量的地址不一样,因为他们的作用域不同,所以变成了新变量定义
| 变量类型 | 默认值 |
|---|---|
| int | 0 |
| bool | false |
| string | "" |
| *int | nil |
| []int | nil |
| map[string] int | nil |
| chan int | nil |
| func(string) int | nil |
| error(error是接口) | nil |
2.3.3变量使用的注意事项
局部变量定义不使用,编译会直接不通过,而全局变量不会
由于上面这条定义,在一些函数返回的时候,有些返回值不会使用到,所以我不能用一个有名字的变量去接受它,后续会有很多麻烦。
package main
import "fmt"
func main() {
_,numb,strs := numbers() //只获取函数返回值的后两个
fmt.Println(numb,strs)
}
//一个可以返回多个值的函数
func numbers()(int,int,string){
a , b , c := 1 , 2 , "str"
return a,b,c
}
2.3.4空标识符_
—空标识符通常作为忽略占位符只用,可用作表达式左值,用于代表无法读取的内容或者不需要知道的内容,避免后续变量命名但却没有使用的语法错误。用来临死规避编译器对未使用变量和导入包的错误检查
import "strconv"
import "fmt"
func main(){
x,_:=strconv.Atoi("12")
fmt.println(x)
}
2.3.5变量的类型转换
go语言中总是要求我们使用显式 的类型转化,
标准库strconv可以在不同进制(字符串)见转换
import "strconv"
func main(){
a,_:=strconv.ParseInt("110110",2,32)
println("0b"+strconv.FormatInt(a,2))
}
有两个别名,别名类型无需转换,可以直接赋值
byte alias for uint8
rune alias for int32
但是并不是底层结构相同的就属于别名,例如64位平台上的int 和int64结构完全一致,也分属不同的类型,需显式转化
常见语法歧义:如果转换的是指针,单向通道或者没有返回值的函数(即使有返回值也推荐用括号),那么必须使用括号,以避免造成的语法分解错误
func main(){ x:=100 p:=*int(&x) //cannot convert &x(type *int) to type int //正确的做法时 p:=(*int)(&x)//让编译器解析为指针类型 类似于(func ())(x) println(p)}
2.3.6自定义类型
使用关键字type定义用户自定义类型,可以是结构体类型,函数类型等
package main
import "fmt"
type (
user struct {
name string
age uint8
}
event func(string) bool
)
func main() {
u := user{"tom", 12}//注意这里是大括号
fmt.Println(u)
var f event = func(s string) bool {
fmt.Println(s)
return s != ""
}
f("abc")
}
注意即便制定了基础类型,也只表明他们有相同的底层结构,二者之间没有任何关系,不能用作别名,不能隐式转化,不能直接用于比较表达式
func main(){
type data int
var d data=10
var x int=d
println(x) //cannot use d as type int in assignment
println(d==x) //invalid operation:d==x(mismatched types data and int)
}
2.4go语言的常量与枚举(enum)
常量形式:
const identifier [type] =value
- 显式类型定义:
const b string = "abc" - 隐式类型定义:
const b = "abc"
const也能用作枚举,常量能够使用一些内置函数比如len,cap,unsafe.sizeof等
package mainimport "unsafe"const ( a = "abc" b = len(a) c = unsafe.Sizeof(a))func main(){ println(a, b, c)}
iota是一个特殊常量,用在枚举的时候可以自增的情况
package main
import "fmt"
func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}
//iota只能在同一个常量组内递增
在定义常量组时,如果不提供初始值,则表示将使用上行的表达式。
package main
import "fmt"
const (
a = 1
b
c
d
)
func main() {
fmt.Println(a)
// b、c、d没有初始化,使用上一行(即a)的值
fmt.Println(b) // 输出1
fmt.Println(c) // 输出1
fmt.Println(d) // 输出1
}
常量与变量的不同之处?
除了常量只读以外,还有什么地方和变量不同呢?不同于变量在运行期间分配存储内存,常量在go在预处理阶段直接展开,作为指令数据使用,所以找不到它的地址。常量即使不适用也不会报错
var x=0x100
const y=0x200
func main(){
println(&x,x)
println(&y,y)
}
//error:can't not take place address of y
2.5 复合类型的初始化
对符合类型的变量的初始化时,有一些语法限制
- 初始化表达式必须含类型标签
- 多个成员以逗号分隔
- 允许多行,但是每行必须以都好或者花括号结束
type data struct{ x int s string}//正确的表达var a data=data{ 1, "abc",}//错误的表达方式var a data ={ 1, "abc",}
2.6变量的作用域的问题
全局变量可以在整个包或者外部包(被导入后)使用
但是我们常见的花括号的内部和外部的变量是不一样的,哪怕他们同名
作用域链的概念就在这里体现了:内部的作用域可以引用外部的作用域的变量,并且覆盖掉它,但是外部的的作用域不能使用内部的变量
package main
import "fmt"
func VariableAreaTest() {
for i := 0; i < 10; i++ {
fmt.Println(i)
i := 0
fmt.Println(i)
}
}
上述的这个循环不会死循环!因为外部的i和内部的i虽然同名但是作用域不同,第二个println由于i被覆盖掉所以还是输出0。
更正一下:上述之所以不会死循环,是因为我在循环体内用了:=初始化声明,所以相当于重新命名和生成了一个新变量,如果直接使用=其实还是调用的外面作用域的i这样子也能像其他语言一样跳过某些循环步骤。
3.golang的表达式和流程控制
3.1go语言的运算符
3.1.1算术运算符
下表列出了所有Go语言的算术运算符。假定 A 值为 10,B 值为 20。
| 运算符 | 描述 | 实例 |
|---|---|---|
| + | 相加 | A + B 输出结果 30 |
| - | 相减 | A - B 输出结果 -10 |
| * | 相乘 | A * B 输出结果 200 |
| / | 相除 | B / A 输出结果 2 |
| % | 求余 | B % A 输出结果 0 |
| ++ | 自增 | A++ 输出结果 11 |
| -- | 自减 | A-- 输出结果 9 |
| 运算符 | 描述 | 实例 |
|---|---|---|
| == | 检查两个值是否相等,如果相等返回 True 否则返回 False。 | (A == B) 为 False |
| != | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 | (A != B) 为 True |
| 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 | (A > B) 为 False | |
| < | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 | (A < B) 为 True |
| >= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 | (A >= B) 为 False |
| <= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 | (A <= B) 为 True |
3.1.2逻辑运算符
下表列出了所有Go语言的逻辑运算符。假定 A 值为 True,B 值为 False。
| 运算符 | 描述 | 实例 |
|---|---|---|
| && | 逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。 | (A && B) 为 False |
| || | 逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。 | (A || B) 为 True |
| ! | 逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 | !(A && B) 为 True |
3.1.3位运算符
左移和右移都需要将该变量转化成无符号形式的
| 运算符 | 描述 | 实例 |
|---|---|---|
| & | 按位与运算符"&"是双目运算符。 其功能是参与运算的两数各对应的二进位相与。 | (A & B) 结果为 12, 二进制为 0000 1100 |
| | | 按位或运算符"|"是双目运算符。 其功能是参与运算的两数各对应的二进位相或 | (A | B) 结果为 61, 二进制为 0011 1101 |
| 按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 | (A ^ B) 结果为 49, 二进制为 0011 0001 | |
| << | 左移运算符"<<"是双目运算符。左移n位就是乘以2的n次方。 其功能把"<<"左边的运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补0。 | A << 2 结果为 240 ,二进制为 1111 0000 |
| >> | 右移运算符">>"是双目运算符。右移n位就是除以2的n次方。 其功能是把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数。 | A >> 2 结果为 15 ,二进制为 0000 1111 |
和python一样可以组合运算符和赋值符号
//一个特殊的bit clear,将左右操作数对用二进制位都是1的重置为0,以达到一次清楚多个标记位的目的
const{
read byte = 1 << iota
write
exec
freeze
}
func main(){
a := read | write | freeze
b := read | freeze | exec
c := a &^ b
fmt.Printf("%04b &^ %04b =%04b\n", a, b, c)
}
//1011 &^ 1101 =0010输出???
3.1.4其他
| 运算符 | 描述 | 实例 |
|---|---|---|
| & | 返回变量存储地址 | &a; 将给出变量的实际地址。 |
| * | 指针变量。 | *a; 是一个指针变量 |
3.1.5运算符的优先级
| 优先级 | 运算符 |
|---|---|
| 5 | * / % << >> & &^ |
| 4 | + - | ^ |
| 3 | == != < <= > >= |
| 2 | && |
| 1 | || |
别忘记括号
3.1.6自增不再是表达式,为独立语句
func main(){ a:=1 a++ //现在++不能后置,只能前置 if (++a)>1 { } p:=&a *p++//相当于(*p)++ println(a)}
表达式通常是求值代码,可作为右值或者参数使用,而语句完成一个行为,比如if,for代码块,表达式可以当作语句用,而语句不能当作表达式
3.1.7指针
不要搞混指针和内存地址,指针是一个保存地址的整型变量,需要内存空间。
- 指针不能用于运算+-,并且不能类型转换
- &获取地址,*引用对象
- **T二级指针,如包含包名则写成 *package.T
unsafe.Pointer可以将指针转化成unintptr后进行加减法运算,但是可能会造成非法访问,golang没有指针运算!
Pointer类似于C语言的*void万能指针,用来转换指针类型,它能安全持有对象或对象成员,但uintptr不行,它仅仅是一种特殊整形对象,并没有引用目标对象,无法阻止垃圾回收器回收对象内存
指针没有专门指向成员的->运算符,和python一样用.
3.2go语言的条件语句
go语言没有三目运算符,不能用 ? : 这样的形式
| 语句 | 描述 |
|---|---|
| if 语句 | if 语句 由一个布尔表达式后紧跟一个或多个语句组成。 |
| if...else 语句 | if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。 |
| if 嵌套语句 | 你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。 |
| switch 语句 | switch 语句用于基于不同条件执行不同动作。 |
| select 语句 | select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。 |
switch语句在每个case后面自动提供break语句,如果想要继续运行,就要在每个case的最后加上一个fallthrough才能继续运行下一个case并且不判断条件了,并且不限定case为数字,也不一定是常量,甚至可以是变量。并且switch的执行顺序是顺序的
//switchyu
func main() {
fmt.Print("Go runs on ")
osstr:="linux"
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
fallthrough
case osstr:
fmt.Println("same")
fallthrough
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}
//output
特别注意最后一种select语句
//select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
//select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
为了将一长串的if-else写的更清晰,可以使用无条件的switch
package main
import (
"fmt"
"time"
)
func main() {
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
//fallthrough语句switch x :=5;x{
default:
println(x)
case 5: x+=10
println(x)
fallthrough//继续执行下一case,但是不再匹配条件表达式
case 6:
x+=20
println(x)
//fallthrough //如果在此继续fallthrough,不会执行default,完全按源码顺序}
以下描述了 select 语句的语法: -每个 case 都必须是一个通信 -所有 channel 表达式都会被求值 -所有被发送的表达式都会被求值 -如果任意某个通信可以进行,它就执行,其他被忽略。 -如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。 -如果有 default 子句,则执行该语句。 -如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。
3.3go语言的循环语句
func main() {
/* 定义局部变量 */
var i, j int
for i=2; i < 100; i++ {
for j=2; j <= (i/j); j++ {
if(i%j==0) {
break; // 如果发现因子,则不是素数
}
}
if(j > (i/j)) {
fmt.Printf("%d 是素数\n", i);
}
}
}
不过就是java的去掉了括号,continue可以添加标记,break也是
fmt.Println("---- continue label ----")
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue re
}
}
----------------------------------------------------
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
break re
}
}
range迭代
range类似于一个迭代器,可以获得索引和键值数据,注意range会复制目标数据,这是因为相关的数据类型中,字符串和切片基本结构都是很小的结构体,而字典,通道本身是指针封装,复制成本都很小,无需专门优化
当使用 for 循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
package main
import "fmt"
func main() {
data := [3]string{"a", "b", "c"}
for i, s := range data {
fmt.Println(i, s)
}
data1 :=[]int{10,20,30}
for i,x :=range data1[:]{//这里拿到的x是复制出来的值
if i==0 {
data[0]+=100
data[1]+=200
data[2]+=300
}
fmt.Printf("x:%d,data:%d\n",x,data[i])
}
}
没有相关的接口可以实现自定义类型的迭代,除非基础类型是可迭代类型,例如字符串,数组,数组指针,切片,字典,通道类型
goto语句
goto不能跳转到其他函数或者代码块内
4.go语言的函数
函数属于第一对象(指在运行期创建,可以用作函数参数或这返回值,可存入变量的实体,最常见的就是匿名函数),具备相同签名(参数及返回值列表) 的视作统一类型
- 无需前置声明
- 不支持命名嵌套定义
- 不支持同名函数重载!!!
- 不支持默认参数!!!
- 不支持不定长变参!!!!
- 支持匿名函数和闭包
- 不支持有默认值的可选参数,不支持命名实参
func test(x,y int,s string,_ bool) *int{
return nil
}
func main(){
test(1,2,"abc")//错误,即便是_命名的参数也不能忽略
}
func function_name( [parameter list] ) [return_types] { 函数体}
函数定义解析:
- func:函数由 func 开始声明
- function_name:函数名称,函数名和参数列表一起构成了函数签名。
- parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 函数体:函数定义的代码集合。
函数的参数调用有两种方式,一种是值传递,值传递不会改变原来函数的参数,一种是引用传递,下面是引用传递的例子
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 )
/* 调用 swap() 函数
* &a 指向 a 指针,a 变量的地址
* &b 指向 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 */
}
2.8.1函数作为参数传入
package main
import (
"fmt"
"math"
)
func main(){
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9))
}
2.8.2函数的递归
//用函数递归写出斐波那契数列
func fibornachi(i int) int {
if i < 2 {
return i
}
return fibornachi(i-1) + fibornachi(i-2)
}
fmt.Println(fibornachi(8))
2.8.3函数闭包
Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。
以下实例中,我们创建了函数 getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量,代码如下:
package main
import "fmt"
func closure(i int) func(j int) int {
sum := 1
return func(i int) int {
for j := 1; j < i; j++ {
sum *= j
}
fmt.Print("number", sum) //生成一个字符串不打印
return sum
}
}
func main(){
closure(10)(11)
}
最后输出的是11的阶乘,闭包可以防止外面那个函数的变量被回收
package main
import "fmt"
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
func main(){
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()
/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}
实现一个 fibonacci 函数,它返回一个函数(闭包),该闭包返回一个斐波纳契数列 (0, 1, 1, 2, 3, 5, ...)。
package main
import "fmt"
// 返回一个“返回int的函数”
func fibonacci() func() int {
a:=make([]int,11)
i:=0
return func() int{
i++
if i<2{
a[i]=1
return 1
}
a[i]=a[i-1]+a[i-2]
return a[i]
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
2.8.4 defer
defer语句的作用就是把推迟的函数调用压入一个栈中,被推迟的函数会按照先进后出的顺序调用。为了更好的理解让我们一起看一个例子
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
dst, err := os.Create(dstName)
if err != nil {
return
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
return
}
上面例子看起来没有什么问题,其实有个Bug,如果os.Create出错,那么src打开的文件不会被回收。
func CopyFile(dstName, srcName string) (written int64, err error) {
src, err := os.Open(srcName)
if err != nil {
return
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
return
}
defer dst.Close()
return io.Copy(dst, src)
}
解决方法如上,需要知道几个概念
- defer运行是在外部函数的return之后的
- defer的运行是后进先出的
- defer可以调用返回函数中的命名变量值
package main
import "fmt"
func inc() (i int) {
defer func() { i++ }()
return i
}
func main(){
fmt.Println(inc())
}
//output: 1
5.go数据结构
5.1字符串
字符串是不可变字节序列,本身是一个复合结构,和python中的字符串一样,可以切片和索引,因为底层是数组
//头部指针指向字节数组,但没有NULL结尾
type stringStruct struct{
str unsafe.Pointer
len int
}
//使用`定义不做转义处理的字符串可以跨行,类似于python的r
s:=`line1\r\n,
line2`
5.1.1字符串的两种遍历方式
\\byte
s1:="山东大学"
for i := 0; i < len(s1); i++ {
fmt.Printf("%d:[%c]\n", i, s1[i])
}
/*输出结果
0:[å]
1:[±]
2:[±]
3:[ä]
4:[¸]
5:[]
6:[å]
7:[¤]
8:[§]
9:[å]
10:[]
11:[¦]
*/
//rune
for i,item:=range s1 {
fmt.Printf("%d:[%c]\n", i, s1[i])
}
要修改字符串,必须将其转化成可变类型[]rune,[]byte,待完成后再转化回来,需要重新分配内存,复制数据
5.2数组
数组声明
var variable_name [SIZE] variable_type
var array1 [5] int //这时候默认是 00000
var array2:= [5] int {1,2,3,4,5}//
//错误
array1={1,2,3,4,5}
-数组的初始化与声明是同时的,不能先声明然后初始化
-数组是固定长度的,并且如果[]中没有数字就不是数组,而是切片slide
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] )
}
}//数组初始化的方法
5.3切片
5.3.1切片的声明和初始化
切片其实是没有定长的数组,切片和数组十分相似,如果试图使用{}来进行赋值就只能在初始化的时候才能使用.
切片并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
与它共享底层数组的切片都会观测到这些修改
var identifier []type
切片的初始化有两种形式
make也有两种方式
s:=[]int{1,2,3}
s:=make([] int,3)//这里即是在声明同时也初始化了,只不过每个元素都是默认值
//length是初始化的时候的长度,capacity是数组的最大容量
make([]T,length)
make([]T, length, capacity)
//len()可以获取长度,cap()可以获取容量
那么切片如果只是声明了没有初始化是怎么样的呢
package main
import "fmt"
func main() {
var numbers []int
printSlice(numbers)
if(numbers == nil){
fmt.Printf("切片是空的")
}
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
//切片声明了就默认是是nil,长度和容量默认都是0
//切片的截取同python
切片练习:
package main
import "golang.org/x/tour/pic"
//import "reflect"
import "fmt"
func Pic(dx, dy int) [][]uint8 {
Pic:=make([][] uint8,dy*dx)
fmt.Println(len(Pic))
PicCol:=make([] uint8,dx)
for i:=range PicCol{
PicCol[i]= 1
}
for i:= range Pic {
//fmt.Println(reflect.TypeOf(col))
Pic[i] = PicCol
}
//Pic为什么不用输入参数??
return Pic
}
func main() {
pic.Show(Pic)
}
//我发现只能用索引号来拿到切片内容,下面这种方式行不通,报错显示我没有使用item,但是如果print这个item它却认为我有使用??
for _,item := range slice{
item=11
}
5.3.2切片的copy和append
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)
}
/*
len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]*/
5.4map
map是无序的键值对的集合,可以迭代
var country map[string]string
//下面这句一定要写,他是分配空间的?否则会报错assingment is nil map
country = make(map[string]string)
country["france"] = "法国"
country["english"] = "英国"
for key, item := range country {
fmt.Println(key, item)
}
for key := range country {
fmt.Println(key)
}
item, ok := country["china"]
fmt.Println(item, ok)
//delete 函数用于删除对应的键值对
delete(country,"france")
//输出
france 法国
english 英国
france
english
false
map练习,输入一个字符串,然后返回一个map里面包含了每个元素和元素的个数的映射
package main
import (
"golang.org/x/tour/wc"
"strings"
)
func WordCount(s string) map[string]int {
strList:=strings.Fields(s)
wordcount:=make(map[string]int)
for i :=range strList{
var count int
for j:=range strList{
if strList[i]==strList[j]{
count++
}
}
wordcount[strList[i]]=count
}
return wordcount
}
func main() {
wc.Test(WordCount)
}
5.5结构体
结构体的定义和声明
type struct_variable_type struct {
member definition
member definition
...
member definition
}
声明的时候需要注意一点,必须在花括号前加上结构体的名字
variable_name := structure_variable_type {value1, value2...valuen}
或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
package main
import "fmt"
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 1} // Y:0 被隐式地赋予
v3 = Vertex{} // X:0 Y:0
p = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)
func main() {
fmt.Println(v1, p, v2, v3)
}
type mystruct struct{
name string
age int
}
var myself =mystruct {age: 11,name: "uhu"}
fmt.Println(myself)
//错误的改变数据的方式
myself=mystruct{12,"hhh"}
//正确的
myself.age=12