title: go语言基础(一)
date: 2021-04-07 16:58:12
categories:
由于自身go语言基础不扎实,导致一些基本的语法不懂,在学习fabric过程中会出现各种问题,影响学习效率。
此文章以及后续一系列go语言基础文章,将帮助自己重新学习go语言,并作为自己go语言学习笔记。
简单
并发模型
go语言从根部将一切都并发化,运行时用Goroutine运行所有的一切,包括main.main入口函数。Goroutine是go的显著特征。它用类协程的方式处理并发单元,又在运行时层面做了更深度的优化处理。搭配channel,实现CSP模型。
csp模型
Actor 模型中 Actor 之间就是不能共享内存的,彼此之间通信只能依靠消息传递的方式。Golang 实现的 CSP 模型和 Actor 模型看上去非常相似,虽然 Golang 中协程之间,也能够以共享内存的方式通信,但是并不推荐;而推荐的以通信的方式共享内存,实际上指的就是协程之间以消息传递方式来通信。
Channel模型中,worker之间不直接彼此联系,而是通过不同channel进行消息发布和侦听。消息的发送者和接收者之间通过Channel松耦合,发送者不知道自己消息被哪个接收者消费了,接收者也不知道是哪个发送者发送的消息。
Go语言的CSP模型是由协程Goroutine与通道Channel实现:
- Go协程goroutine: 是一种轻量线程,它不是操作系统的线程,而是将一个操作系统线程分段使用,通过调度器实现协作式调度。是一种绿色线程,微线程,它与Coroutine协程也有区别,能够在发现堵塞后启动新的微线程。
- 通道channel: 类似Unix的Pipe,用于协程之间通讯和同步。协程之间虽然解耦,但是它们和Channel有着耦合。
内存分配
刨去因配合垃圾回收器而修改的内容,内存分配器完整的保留了tcmalloc的原始架构。除偶尔因性能问题而被迫采用对象池和自主内存管理外,我们基本无须参与内存管理操作。
垃圾回收
go垃圾回收不咋地
只须编译一个可执行文件,无须附加任何东西就能部署。将运行时、依赖库直接打包到可执行文件内部,简化了部署和发布操作,无须事先安装运行环境和下载诸多第三方库。
var a int //会自动初始化为0
var y=false //自动推断为bool类型
var x,y int
x=1
y=2 //定义完变量后再赋值
var a int =2
var a,s=100,"abc" //初始化
var (
x,y int
a,s=100,"abc" //字符串加“”
)
a:=100 //自动推导类型
a,s:=100,"abc"
注意:
* 定义变量,同时显示初始化
* 不能提供数据类型
* 只能用在函数内部
退化赋值的前提条件是:最少有一个新变量被定义,且必须是同一作用域。
fun main(){
x:=100
x,y:=200,"abc" //退化赋值操作,只有y是变量定义
}
fun main(){
x:=100
x:=200 //错误
}
在处理函数错误返回值时,退化赋值允许我们重复使用err变量。
fun main(){
x,y:=1,2
x,y=y+2,x+2
}
4 3
匿名变量,丢弃数据不进行处理, _匿名变量配合函数返回值使用才有价值.
_,i,_,j:=1,2,3,4
编译器将未使用的变量当作错误。
符号名字首字母大小写决定了其作用域。首字母大写的为导出成员,可被包外引用,而小写则仅能在包内使用。
通常作为忽略占位符使用,可作为表达式左值,无法读取内容。可用来临时规避编译器对未使用变量和导入包的错误检查。但它是预置成员,不能重新定义。
x,_:=strconv.Atoi("12")
fmt.println(x)
常量值必须是编译器可以确定对字符、字符串、数字或布尔值。
代码中不使用的常量不会发生编译错误,与变量不同。
const x,y int=123,0x22
const s = "hello,world"
const x = '点点滴滴' //错误
const (
i,f =1,0.123 //int , float64(默认)
b =false
)
const (
x uint16=12
y //与x类型,右值相同
s ="abc"
z //与s类型,右值相同
)
const (
ptrsize=unsafe.Sizeof(uintptr(0)) //返回数据类型的大小 uintptr是一个整数类型
strsize=len("hello,world!") //len返回长度,表示有几个元素,cap返回指定类型的容量,类型不同意义不同。
)
const (
x,y int =99,-999
b byte =byte(x) // x被指定为int类型,须显式转换为byte类型
n =uint8(y) //错误 右值不能超过常量类型的取值范围。
)
数字类型变量的字节数和取值范围如下:
<< 左移运算符将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)。
const(
x = iota //0 自增
y //1
z //2
)
const(
_ = iota //0
KB=1 <<(10*iota) //1<<(10*1)
MB //1<<(10*2)
GB //1<<(10*3)
)
const(
_,_ =iota,iota*10 //0,0*10
a,b //1,1*10
c,d //2,2*10
)
const(
a =iota //0
b //1
c =100 //100
d //100
e =iota //4(恢复itoa自增,计数包括c,d)
f //5
)
自增默认数据类型为int,可显式指定类型。
const(
a =iota //int
b float32 =iota //float32
c =iota //int (如果不指定iota,则与b数据类型相同)
)
在实际编码中,建议用自定义类型实现用途明确的枚举类型。但这并不能将取值范围限定在预定义的枚举值内。
type color byte //自定义类型 byte取值范围 -128-127
const(
black colot =iota //指定常量类型
red
blue
)
不同于变量在运行期分配存储内存(非优化状态),常量通常会被编译器在预处理阶段直接展开,作为指令数据使用。
就是说常量不会分配存储空间,无法获取地址。
类型 | 长度 | 默认值 | 说明 |
---|---|---|---|
bool | 1 | false | |
byte | 1 | 0 | uint8 |
int,uint | 4,8 | 0 | 默认整数类型,依据目标平台,32或64位 |
int8,uint8 | 1 | 0 | -128127,0255 |
int16,uint16 | 2 | 0 | -3276832767,065535 |
int32,uint32 | 4 | 0 | -21亿~21亿,0~42亿 |
int64,uint64 | 8 | 0 | |
float32 | 4 | 0.0 | |
float64 | 8 | 0.0 | 默认浮点数类型 |
complex64 | 8 | ||
complex128 | 16 | ||
rune | 4 | 0 | Unicode Code Point,int32 |
uintptr | 4,8 | 0 | 足以存储指针的uint |
string | “” | 字符串,默认值为空字符串,而非NULL | |
array | 数组 | ||
struct | 结构体 | ||
function | nil | 函数 | |
interface | nil | 接口 | |
map | nil | 字典,引用类型 | |
slice | nil | 切片,引用类型 | |
channel | nil | 通道,引用类型 |
strconv包提供了字符串与简单数据类型之间的类型转换功能。可以将简单类型转换为字符串,也可以将字符串转换为其它简单类型。
golang strconv**.ParseInt** 是将字符串转换为数字的函数,功能灰常之强大,看的我口水直流.
func ParseInt(s string, base int, bitSize int) (i int64, err error)
参数1 数字的字符串形式
参数2 数字字符串的进制 比如二进制 八进制 十进制 十六进制
参数3 返回结果的bit大小 也就是int8 int16 int32 int64
byte alias for uint8
rune alias for int32
别名类型无需转换,可以直接赋值。
格式化指令 | 含义 |
---|---|
%% | 字面上的百分号,并非值的占位符 |
%b | 一个二进制整数,将一个整数格式转化为二进制的表达方式 |
%c | 一个Unicode的字符 |
%d | 十进制整数 |
%o | 八进制整数 |
%x | 小写的十六进制数值 |
%X | 大写的十六进制数值 |
%U | 一个Unicode表示法表示的整型码值 |
%s | 输出字符串表示(string类型或[]byte) |
%t | 以true或者false的方式输出布尔值 |
%q | 双引号围绕的字符串,由Go语法安全地转义 |
%p | 十六进制表示,前缀 0x |
%T | 相应值的类型 |
%v | 只输出所有的值 相应值的默认格式 |
%+v | 先输出字段类型,再输出该字段的值 |
%#v | 先输出结构体名字值,再输出结构体(字段类型+字段的值) |
# | 备用格式:为八进制添加前导 0(%#o)。 为十六进制添加前导 0x(%#x) |
具体看下面链接
https://blog.csdn.net/zp17834994071/article/details/108619759
package main
import (
"fmt"
"math"
)
func main() {
/*
取绝对值,函数签名如下:
func Abs(x float64) float64
*/
fmt.Printf("[-3.14]的绝对值为:[%.2f]\n", math.Abs(-3.14))
/*
取x的y次方,函数签名如下:
func Pow(x, y float64) float64
*/
fmt.Printf("[2]的16次方为:[%.f]\n", math.Pow(2, 16))
/*
取余数,函数签名如下:
func Pow10(n int) float64
*/
fmt.Printf("10的[3]次方为:[%.f]\n", math.Pow10(3))
/*
取x的开平方,函数签名如下:
func Sqrt(x float64) float64
*/
fmt.Printf("[64]的开平方为:[%.f]\n", math.Sqrt(64))
/*
取x的开立方,函数签名如下:
func Cbrt(x float64) float64
*/
fmt.Printf("[27]的开立方为:[%.f]\n", math.Cbrt(27))
/*
向上取整,函数签名如下:
func Ceil(x float64) float64
*/
fmt.Printf("[3.14]向上取整为:[%.f]\n", math.Ceil(3.14))
/*
向下取整,函数签名如下:
func Floor(x float64) float64
*/
fmt.Printf("[8.75]向下取整为:[%.f]\n", math.Floor(8.75))
/*
取余数,函数签名如下:
func Floor(x float64) float64
*/
fmt.Printf("[10/3]的余数为:[%.f]\n", math.Mod(10, 3))
/*
分别取整数和小数部分,函数签名如下:
func Modf(f float64) (int float64, frac float64)
*/
Integer, Decimal := math.Modf(3.14159265358979)
fmt.Printf("[3.14159265358979]的整数部分为:[%.f],小数部分为:[%.14f]\n", Integer, Decimal)
}
特指slice、map、channel这三种预定义类型。相比数字、数组等类型,引用类型拥有更复杂的存储结构。除分配内存外,他们还须初始化一系列属性,诸如、长度,甚至包括哈希分布、数据队列等。
内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型则必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化。
就一句话 slice、map、channel只能用make函数创建。
new函数也可以为引用类型分配内存,但这不是完整的创建。以字典map为例,它仅分配零字典类型本身(实际就是个指针包装)所需内存,并没有分配键值存储内存,也没有初始化散列桶等内部属性,因此它无法正常工作。
func main(){ p:=new(map[string]int) //函数new返回指针 m:=*p m["a"]=1 //错误 fmt.println(m) }
go强制要求使用显示类型转换。
a :=10
b :=byte(a)
c :=a + int(b) //混合类型表达式必须确保类型一致
如果转换的目标是指针、单向通道或没有返回值的函数类型,那么必须使用括号,以避免造成语法分解错误。
func main(){
x :=100
p :=*int(&x) //错误
p :=(*int)(&x) // 让编译器将*int解析为指针类型
println(p)
}
使用关键字type定义用户自定义类型。
即便指定了基础类型,也只表明它们有相同底层数据结构,两者间不存在任何关系,属于完全不同的两种类型。除操作符外,自定义类型不会继承基础类型的其他信息(包括方法)。不能视作别名,不能隐式转换,不能直接用于比较表达式。
func main(){ type data int var d data =10 var x int = d //错误 println(x) println(d ==x) //错误 }
数组、切片、字典、通道等类型与具体元素类型或长度等属性有关,故称作未命名类型。可用type为其提供具体名称,将其改变为命名类型。
具有相同声明的未命名类型被视作同一类型。
- 具有相同基类型的指针
- 具有相同元素类型和长度的数组
- 具有相同元素类型的切片
- 具有相同键值类型的字典
- 具有相同数据类型及操作方向的通道
- 具有相同字段序列的结构体
- 具有相同签名的函数
- 具有相同方法集的接口
未命名类型转换规则:
go语言仅25个保留关键字(keyword)。
没有乘幂和绝对值运算符,对应的是标准库math里的Pow、Abs函数实现。
假定 A 值为 10,B 值为 20。
运算符 | 描述 | 实例 |
---|---|---|
+ | 相加 | A + B 输出结果 30 |
- | 相减 | A - B 输出结果 -10 |
* | 相乘 | A * B 输出结果 200 |
/ | 相除 | B / A 输出结果 2 |
% | 求余 | B % A 输出结果 0 |
++ | 自增 | A++ 输出结果 11 |
– | 自减 | A-- 输出结果 9 |
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
== | 相等于 | 4 == 3 | false |
!= | 不等于 | 4 != 3 | true |
< | 小于 | 4 < 3 | false |
> | 大于 | 4 > 3 | true |
<= | 小于等于 | 4 <= 3 | false |
>= | 大于等于 | 4 >= 1 | true |
运算符 | 术语 | 示例 | 结果 |
---|---|---|---|
! | 非 | !a | 如果a为假,则!a为真; 如果a为真,则!a为假。 |
&& | 与 | a && b | 如果a和b都为真,则结果为真,否则为假。 |
|| | 或 | a || b | 如果a和b有一个为真,则结果为真,二者都为假时,结果为假。 |
有逻辑运算符连接的表达式叫做逻辑表达式
位运算符对整数在内存中的二进制位进行操作。
下表列出了位运算符 &, |, 和 ^ 的计算:
p | q | p & q | p | q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
假定 A = 60; B = 13; 其二进制数转换为:
A = 0011 1100
B = 0000 1101
-----------------
A&B = 0000 1100
A|B = 0011 1101
A^B = 0011 0001
Go 语言支持的位运算符如下表所示。假定 A 为60,B 为13:
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与运算符"&"是双目运算符。 其功能是参与运算的两数各对应的二进位相与。 | (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 |
位移右操作数必须是无符号整数,或可以转换的无显式类型常量。
func main(){ b:=23 //b是有符号int类型变量 a:=1 << b //错误 println(a) }
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,将一个表达式的值赋给一个左值 | C = A + B 将 A + B 表达式结果赋值给 C |
+= | 相加后再赋值 | C += A 等于 C = C + A |
-= | 相减后再赋值 | C -= A 等于 C = C - A |
*= | 相乘后再赋值 | C *= A 等于 C = C * A |
/= | 相除后再赋值 | C /= A 等于 C = C / A |
%= | 求余后再赋值 | C %= A 等于 C = C % A |
<<= | 左移后赋值 | C <<= 2 等于 C = C << 2 |
>>= | 右移后赋值 | C >>= 2 等于 C = C >> 2 |
&= | 按位与后赋值 | C &= 2 等于 C = C & 2 |
^= | 按位异或后赋值 | C ^= 2 等于 C = C ^ 2 |
|= | 按位或后赋值 | C |= 2 等于 C = C | 2 |
运算符 | 描述 | 实例 |
---|---|---|
& | 返回变量存储地址 | &a; 将给出变量的实际地址。 |
* | 指针变量。 | *a; 是一个指针变量 |
有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:
优先级 | 运算符 |
---|---|
5 | * / % << >> & &^ |
4 | + - | ^ |
3 | == != < <= > >= |
2 | && |
1 | || |
type data struct{
x int
s string
}
d:=data{
1,
"abc" //错误,须以逗号或者右花括号结束
}
比较特别的是对初始化语句的支持,可定义块局部变量或执行初始化函数。
func main(){ x:=10 if xinit();x==0{ //优先执行xinit函数 println("a") } if a,b :=x+1,x+10; a<b{ //定义一个或多个局部变量(也可以是函数返回值) println(a) }else{ println(b) } }
局部变量的有效范围包含整个if/else块。
死代码:是指永远不会执行的代码,可使用专门的工具或用代码覆盖率测试进行检查。
switch-case结构语法如下:
switch 变量或者表达式的值{
case 值1:
要执行的代码
case 值2:
要执行的代码
case 值3:
要执行的代码
…………………………………
default:
要执行的代码
}
func main(){
a,b,c,d,x:=1,2,3,2
switch x {
case a,b: //多个匹配条件中其一即可。
println("a | b")
case c:
println("c")
case 4:
println("d")
default:
println("z")
}
}
输出:a | b
switch 同样支持初始化语句,按从上到下、从左到右顺序匹配case执行。只有全部匹配失败时,才会执行default块。
func main(){
switch x:=5;x{
default: //不会先执行这个
x+=100
println(x)
case 5:
x +=50
println(x)
}
}
相邻的空case不构成多条件匹配。
switch x{
case a: //隐式:case a : break
case b:
println(c)
}
无须显式执行break语句,case执行完毕后自动中断。如需贯通后续case,须执行fallthrough,但不再匹配后续条件表达式。fallthrough必须放在case块结尾,可用break语句阻止。
func main{
switch x:=5;x{
default:
println(x)
case 5:
x +=10
//break 终止 不再执行后续语句
fallthrough //继续执行下一case,不在匹配条件表达式 也不会执行dēfault
case 6:
x +=3
println(x)
}
}
语法结构如下:
for 表达式1;表达式2;表达式3{
循环体
}
可用for…range完成数据迭代。
允许返回单值
无论是for循环,还是range迭代,其定义的局部变量都会重复使用。
func main(){
data :=[3]string{"a","b","c"}
for i,s:=range data{
println(&i,&s)
}
}
输出: //重复使用地址。
0xc82003fe98 0xc82003fec8
0xc82003fe98 0xc82003fec8
0xc82003fe98 0xc82003fec8
range会复制目标数据
func main(){
data := [3]int{10,20,30}
for i,x :=range data { //从data复制品中取值
if i==0 {
data[0] +=100
data[1] +=200
data[2] +=300
}
fmt.printf("x: %d,data: %d\n",x,data[i])
}
for i,x :=range data[:]{
if i ==0{
data[0] +=100
data[1] +=200
data[2] +=300
}
fmt.printf("x: %d,data: %d\n",x,data[i])
}
}
输出:
x:10,data:110 //range返回的依旧是复制值
x:20,data:220
x:30,data:330
x:110,data:210 //当i==0修改data时,x已取值,所以是110
x:420,data:420 //复制的仅是slice自身,底层array依旧是原对象
x:630,data:630
如果range目标表达式是函数调用,也仅被执行一次。
select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。
Go 编程语言中 select 语句的语法如下:
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
以下描述了 select 语句的语法:
每个 case 都必须是一个通信
所有 channel 表达式都会被求值
所有被发送的表达式都会被求值
如果任意某个通信可以进行,它就执行,其他被忽略。
如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。
否则:
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")
}
}
以上代码执行结果为:
no communication
控制语句 | 描述 |
---|---|
break 语句 | 经常用于中断当前 for 循环或跳出 switch 语句或select语句。 |
continue 语句 | 仅用于for循环,跳过当前循环的剩余语句,然后继续进行下一轮循环。 |
goto 语句 | 将控制转移到被标记的语句。 |
语法格式如下:
goto label;
..
.
label: statement;
未使用的标签会引发编译错误。
goto 语句流程图如下:
package main
import "fmt"
func main() {
var a int = 10
LOOP: for a < 20 {
if a == 15 {
a = a + 1
goto LOOP
}
fmt.Printf("a的值为 : %d\n", a)
a++
}
}
以上实例执行结果为:
a的值为 : 10
a的值为 : 11
a的值为 : 12
a的值为 : 13
a的值为 : 14
a的值为 : 16
a的值为 : 17
a的值为 : 18
a的值为 : 19
不能跳转到其他函数,或内层代码块
func test(){ test: println("test") } func main(){ for i:=0; i<3; i++{ loop: println(i) } goto test //不能跳转到其他函数 goto loop //不能跳转到内层代码块内 }
Go 语言函数定义格式如下:
func 函数名( 参数列表 ) 返回类型 {
函数体
}
函数只能判断是否为nil,不支持其他比较操作。
func a(){}
func b(){}
func main(){
println(a == nil)
println(a == b) //错误
}
建议命名规则:
go不支持有默认值对可选参数,不支持命名参数。调用时,必须按签名顺序传递指定类型和数量的实参,就算以_命名的参数也不能忽略。
形参是指函数定义中的参数,实参则是函数调用时所传递的参数。行参类似函数的局部变量,而实参则是函数外部对象,可以是常量,变量,表达式或函数等。
参数可视作函数局部变量,因此不能在相同层次定义同名变量。
func add(x,y int)int{
x:=100 //错误
var y int //错误
return x+y
}
不管是指针、应用类型、还是其他类型参数,都是值拷贝传递。区别无非是拷贝目标对象,还是拷贝指针而已。在函数调用前,会为行参和返回值分配内存空间,并将实参拷贝到形参内存。尽管实参和形参都指向同一目标,但传递指针时依然被复制。
func test(p **int){
x:=100
*p=&x
}
func main(){ //二级指针的使用
var p *int
test(&p)
println(*p)
}
变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部。
func test(s string,a ...int){
fmt.printf("%T,%v\n",a,a) //显示类型和值
}
func main(){
test("abc",1,2,3,4)
}
将切片作为变参时,须进行展开操作。如果是数组,先将其转换为切片。
func test(a ...int){
fmt.println(a)
}
func main(){
a:=[3]int{1,2,3}
test(a[:]...)
}
既然变参是切片,那么参数复制的仅是切片自身,并不包括底层数组,也因此可修改原数据。如果需要,可用内置函数copy复制底层数据。
func test(a ...int){
for i:=range a{
a[i] +=100
}
}
func main(){
a:=[]int{10,20,30}
test(a...)
fmt.println(a)
}
输出:
[110 120 130]
有返回值的函数,必须有明确的return终止语句。
除非有panic,或者无break的死循环,则无须return终止语句。
稍有不便的是没有元组类型,也不能用数组、切片接收,但可以用_忽略掉不想要的返回值。多返回值可用作其他函数调用实参,或当作结果直接返回。
匿名函数是指没有定义名字符号的函数。
我们可以在函数内定义匿名函数,形成类似嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。
直接使用
func main(){
func(s string){
println(s)
}("hello,world!") //匿名函数的参数
}
赋值给变量
func main(){
add:=func(x,y int)int{
return x+y
}
println(add(1,2))
}
作为参数
func test(f func()){
f()
}
func main(){
test(func() {
println("hello,world")
})
}
作为返回值
func test()func(int , int) int{
retrun func(x,y int) int{
return x+y
}
}
func main(){
add:=test()
println(add(1,2))
}
将匿名函数赋值给变量,与为普通函数提供名字标识符有着根本的区别。但编译器会为匿名函数生成一个“随机”符号名。
普通函数和匿名函数都可以作为结构体字段,或经通道传递。
除闭包因素外,匿名函数也是一种常见的重构手段。可将大函数分解成多个相对独立的匿名函数块,然后用相对简洁的调用完成逻辑流程,实现框架和细节分离。
相比语句块,匿名函数的作用域被隔离(不使用闭包),不会引发外部污染,更加灵活。没有定义顺序限制,必要时可抽离,便于实现干净、清晰的代码层次。
闭包是在其词法上下文中引用了自由变量的函数,或者说是函数和其引用环境的组合体。
func test(x int) func(){
return func(){
println(x)
}
}
func main(){
f:=test(123)
f()
}
test返回的匿名函数会引起上下文环境变量x。当该函数在main中执行时,它依然可以正确读取x的值,这种现象就称作闭包。
闭包直接引用了原环境变量。返回的不仅仅是匿名函数,还包括所引用的环境变量指针。
正因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。另外,还有所谓“延迟求值”的特性。
语句defer向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作。
func main(){
f,err:=os.open("./main.go")
if err!=nil{
log.Fatalln(err)
}
defer f.close() //仅注册,直到main退出前才执行
... do something ...
}
延迟调用注册的是调用,必须提供执行所需参数(哪怕为空)。参数值在注册时被复制并缓存起来。
func main(){
x,y :=1,2
defer func(a int){
println("defer x,y =",a,y) //y为闭包引用
}(x) //给匿名函数传参 注册时复制调用参数
x +=100
y +=200
println(x,y)
}
输出:
101 202
defer x,y =1 202
延迟调用可修改当前函数命名返回值,但其自身返回值被抛弃。
多个延迟调用安装FILO先进先出次序执行。
编译器通过插入额外指令来实现延迟调用执行,而return和Panic语句都会终止当前函数流程,引发延迟调用。另外,return不是ret汇编指令,它会先更新返回值。
func test() (z int){
defer func(){
println("defer:",z)
z +=100
}()
return 100
}
func main (){
println("test:",test())
}
输出:
defer:100
test:200
标准库将error定义为接口类型,以便实现自定义错误类型。
type error interface{
Error() string
}
error总是最后一个返回参数。标准库提供了相关创建函数,可方便地创建包含简单错误文本的error对象。
错误变量通常以err作为前缀,且字符串内部全部小写,没有结束标点,以便于嵌入到其他格式化字符串中输出。
全局错误变量并非没有问题,因为它们可被用户重新赋值,这就可能导致结果不匹配。
与errors.New类似的还有fat.Errorf,它返回一个格式化内容的错误对象。
自定义错误类型:
type DivError struct{ //自定义错误类型
x,y int
}
func (DivError) Error() string{ //实现error接口方法
return "division by zero"
}
func div(x,y int)(int,error){
if y==0{
return 0,DivError{x,y}
}
return x/y,nil
}
自定义错误类型通常以Error为名称后缀。在用switch按类型匹配时,注意case顺序。应将自定义类型放在前面,优先匹配更具体的错误类型。
大量函数和方法返回error,会使得代码很难看,解决思路有:
- 使用专门的检查函数处理错误逻辑(比如记录日志),简化检查代码。
- 在不影响逻辑的情况下,使用defer延后处理错误状态(err退化赋值)。
- 在不中断逻辑的情况下,将错误作为内部状态保存,等最终“提交”时再处理。
panic会立即中断当前函数流程,执行延迟调用。而在延迟调用函数中,recover可捕获并返回panic提交的错误对象。
func main(){
deefer func(){
if err:=recover();err!=nil{ //捕获错误
log.Fatalln(err)
}
}()
panic("i am dead") //引发错误
println("exit.") //永不会执行
}
error返回的是一般性的错误,但是panic函数返回的是让程序崩溃的错误。
也就是当遇到不可恢复的错误状态的时候,如数组访问越界、空指针引用等,这些运行时错误会引起painc异常,在一般情况下,我们不应通过调用panic函数来报告普通的错误,而应该只把它作为报告致命错误的一种方式。当某些不应该发生的场景发生时,我们就应该调用panic。
一般而言,当panic异常发生时,程序会中断运行。随后,程序崩溃并输出日志信息。日志信息包括panic value和函数调用的堆栈跟踪信息。
当然,如果直接调用内置的panic函数也会引发panic异常;panic函数接受任何值作为参数。
我们在实际的开发过程中并不会直接调用panic( )函数,但是当我们编程的程序遇到致命错误时,系统会自动调用该函数来终止整个程序的运行,也就是系统内置了panic函数。
Go语言为我们提供了专用于“拦截”运行时panic的内建函数——recover。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。
因为Panic参数是空接口类型,因此可以使用任何对象作为错误状态。而recover返回结果同样要做转型才能获得具体信息。
无论是否执行recover,所有延迟调用都会被执行。但中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程奔溃。
字符串是个不可变字节(byte)序列,其本身是一个复合结构。
头部指针指向字节数组,但没有NULL结尾。默认以UTF-8编码存储Unicode字符,字面量里允许使用十六进制、八进制和UTF编码格式。
内置函数len返回字节数组长度,cap不接受字符串类型参数。
字符串默认值不是nil ,而是“”。
使用"`"定义不做转义处理的原始字符串(raw string),支持跨行。
func main(){
s:=`line\r\n,
line 2`
}
输出:
line\r\n,
line 2
编译器不会解析原始字符串内的注释语句,且前置锁进空格也属于字符串内容。
允许索引号访问字节数组(非字符),但不能获取元素地址。
func main(){
s:="abc"
println(s[1])
println(&s[1]) //错误
}
以切片语法(起始和结束索引号)返回子串时,其内部依旧指向原字节数组。
使用for遍历字符串时,分byte和rune两种方式。
func main(){
s:="雨痕"
for i:=0;i
Contains
func Contains (s, substrstring) bool
功能:字符串s中是否包含substr,返回bool值
var str string ="hellogo"
fmt.println(strings.contains(str,"go")) //返回值为true
Join
func Join (a[]string,sepstring) string
功能:字符串链接,把slicea通过sep链接起来
s :=[]string("abc","hello","mike")
buf :=strings.Join(s,"|")
fmt.println("buf=",buf)
输出:
buf=abc|hello|mike
Index
func Index (s,sepstring) int
功能:在字符串s中查找sep所在的位置,返回位置值,找不到返回-1
fmt.println(strings.Index("abcdhello","hello"))
fmt.println(strings.Index("abcdhello","go")) //不包含返回-1
输出:
4
-1
Repeat
func Repeat (sstring,countint) string
功能:重复s字符串count次,最后返回重复的字符串
buf:=strings.Repeat("go",3)
fmt.Println("buf=",buf) //"gogogo"
Replace
func Replace (s,old,newstring,nint)string
功能:在s字符串中,把old字符串替换为new字符串,n表示替换的次数,小于0表示全部替换
fmt.println(string.Replace("oink oink oink","k","ky",2))
fmt.println(string.Replace("oink oink oink","k","moo",-1))
输出:
oinky oinky oink
moo moo moo
Split
func Split (s,sepstring)[]string
功能:把s字符串按照sep分割,返回slice
buf:="hello@go@mike"
s2:=strings.Split(buf,"@")
fmt.println("s2=",s2)
输出:
s2=[hello abc go mike]
Trim
func Trim (sstring,cutsetstring)string
功能:在s字符串的头部和尾部去除cutset指定的字符串
buf:=strings.Trim(" are u ok? "," ") //去掉两头空格
fmt.println("buf=#%s#\n",buf)
输出:
buf=#are u ok?#
Fields
func Fields (sstring)[]string
功能:去除s字符串的空格符,并且按照空格分割返回slice
要修改字符串,须将其转换为可变类型([]rune或[]byte),待完成后再转换回来。但不管如何转换,都须重新分配内存,并复制数据。
相应的字符串转换函数都在”strconv”包。
Format 系列函数把其他类型的转换为字符串。
//将bool类型转换为字符串
var str string
str = strconv.FormatBool(false)
fmt.println(str)
//将整型转换为字符串
var str string
str = strconv.Itoa(666)
fmt.println(str)
//将浮点数转换为字符串
var str string
str = strconv.FormatFloat(3.14,'f',3,64)//'f'指打印格式,以小数方式,3指小数点位数,64以float64处理
fmt.println(str)
Parse系列函数把字符串转换为其他类型
//字符串转化其他类型
var flag bool
var err error
flag,err=strconv.ParseBool("true")
if err==nil{
fmt.println("flag=",flag)
}else{
fmt.println("err=",err)
}
//把字符串转换为整型
a,_:=strconv.Atoi("456")
fmt.println("a=",a)
b,err:=strconv.ParseFlat("123.34",64)
if err ==nil{
fmt.println("flag=",b)
}else{
fmt.println("err=",err)
}
Append 系列函数将整数等转换为字符串后,添加到现有的字节数组中
slice :=make([]byte,0,1024)
slice = strconv.AppendBool(slice,true)
slice = strconv.AppendInt(slice,1234,10) //第二个数为要追加的数,第三个为指定10进制方式追加。
slice = strconv.APPendQute(slice,"abc")
fmt.println("slice=",string(slice)) //转换string后再打印
结果:
slice=true1234"abc"
类型rune专门用来存储Unicode码点(code point),它是int32的别名,相当于UCS-4/UTF-32编码格式。使用单引号的字面量,其默认类型就是rune。
除[]rune 外,还可以直接在rune,byte,string间进行转换。
定义数组类型时,数组长度必须是非负整型常量表达式,长度是类型组成部分。也就是说元素类型相同,但长度不同的数组不属于同一类型。
func main(){
var a [4]int //元素自动初始化为零 [0 0 0 0]
b:=[4]int{2,5} //未提供初始值的元素自动化初始为0 [2 5 0 0]
c:=[4]int{5,3:10} //可指定索引位置初始化 [5 0 0 10]
d:=[...]int{1,2,3} //按初始化值数量确定数组长度 [1 2 3]
e:=[...]int{10,3:100} //支持索引初始化,但注意数组长度与此有关 [10 0 0 100]
}
对于结构等复合类型,可省略元素初始化类型标签。
type user struct{
name string
age byte
}
d:=[...]user{
{"tom",20},
{"mare",23}, //省略了类型标签
}
在定义多维数组时,仅第一维度允许使用”…“。
指针数组:是指元素为指针类型的数组。
数组指针:是获取数组变量的地址。
func main(){
x,y:=10,20
a:=[...]*int{&x,&y} //元素为指针的指针数组
p:=&a //存储数组地址的指针
}
可获取任意元素地址。
a:=[...]int{1,2}
println(&a,&a[0],&a[1])
0xc82003ff20 0xc82003ff20 0xc82003ff28
数组指针可直接用来操作元素。
go数组是值类型,赋值和传参操作都会复制整个数组数据。
切片本身并非动态数组或数组指针。它内部通过指针引用底层数组,设定相关属性将数据读写操作限定在指定区域内。切片本身是个只读对象,其工作机制类似数组指针的一种包装。
切片:切片与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大,所以可以将切片理解成“动态数组”,但是,它不是数组。
type slice struct{
array unsafe.Pointer
len int
cap int
}
s:=[ ]int{ } //定义空切片
s:=[]int{1,2,3} //初始化切片
s =append(s,5,6,7) //通过append函数向切片中追加数据
fmt.println(s)
输出结果:[1 2 3 5 6 7]
var s1 []int //声明切片和声明数组一样,只是少了长度,此为空(nil)切片
//借助make函数, 格式 make(切片类型, 长度, 容量)
s := make([]int, 5, 10)
属性cap表示切片所引用数组片段的真实长度,len用于限定可读的写元素数量。另外,数组必须是addressable,否则会引发错误。
可直接创建切片对象,无须预先准备数组。因为是引用类型,须使用make函数或显示初始化语句,它会自动完成底层数组内存分配。
func main(){
s1:=make([]int,3,5) //指定le、cap,底层数组初始化为零
s2:=make([]int,s) //省略cap,和len相等
s3:=[]int{10,20,5:30} //按初始化元素分配底层数组,并设置len、cap
fmt.println(s3,len(s3),cap(s3))
}
输出:
[10 20 0 0 0 30] 6 6
func main(){ var a []int b:=[]int{} println(a==inl,b==nil) } 输出:true false
前者仅定义了一个[]int类型变量,并未执行初始化操作,而后者则用初始化表达式完成了全部创建过程。
变量b的内部指针被赋值,a==nil,仅表示他是个未初始化的切片对象,切片本身依然会分配所需内存。
不支持比较操作,就算元素类型支持也不行,仅能判断是否为nil
func mian(){
a:=make([]int,1)
b:=make([]int,1)
println(a==b) //错误。不能比较
}
可以获取元素地址,但不能向数组那样直接用指针访问元素内容。
func main(){
s:=[]int{0,1,2,3,4}
p:=&s //取header地址
p0:=&s[0] //取array[0]地址
p1:=&s[1]
println(p,p0,p1)
(*p)[0]+=100 //*[]int 不支持索引操作,须先返回[]int 对象
*p +=100 //直接用元素指针操作
fmt.println(s)
}
输出:
0xc82003ff00 0xc8200141e0 0xc8200141e8
[100 101 2 3 4]
如果元素类型也是切片,那么就可以实现类似交错数组的功能
func main(){
x:=[][]int{
{1,2},
{10,20,30},
{100},
}
fmt.println(x[1])
x[2]=append(x[2],200,300)
fmt.println(x[2])
}
输出:
[10 20 30]
[100 200 300]
切片只是很小的结构体对象,用来代替数组传参可避免复制开销。make函数允许在运行期动态指定数组长度,绕开了数组类型必须使用编译器常量的限制。
并非所有时候都适合用切片代替数组,因为切片底层数组可能会在堆上分配内存。而且小数组在栈上拷贝的消耗也未必就比make代价大。
将切片视作[cap]slice数据源,据此创建新切片对象。不能超出cap,但不受len限制。
s2=s1 [2:4:6]
len:2 cap:4
s[low:high:max]
从切片s的索引位置low到high处所获得的切片,len=high-low,cap=max-low
新建切片对象依旧指向原底层数组,也就是说修改对所有关联切片可见。
func main(){
d:=[...]int{0,1,2,3,4,5,6,7,8,9}
s1:=d[3:7]
s2:=s1[1:3]
for i:=range s2{
s2[i]+=100
}
fmt.println(d)
fmt.println(s1)
fmt.rpintln(s2)
}
输出:
[0 1 2 3 104 105 6 7 8 9]
[3 104 105 6] //就是说 修改会全部修改
[104 105]
向切片尾部(slice[len])添加数据,返回新的切片对象。
数据被追加到原底层数组。如超出cap限制,则为新切片对象重新分配数组
正因为存在重新分配底层数组的缘故,在某些场合建议预留足够多的空间,避免中途内存分配和数据复制开销。
在两个切片对象间复制数据,允许指向同一底层数组,允许目标区间重叠。最终所复制长度以较短的切片长度(len)为准。将第二个切片里面的元素,拷贝到第一个切片中。
返回值为int型,为返回复制的元素个数。
func main(){
s:=[]int{0,1,2,3,4,5,6,7,8,9}
s1:=s[5:8]
n:=copy(s[4:],s1) //在同一底层数组的不同区间复制
fmt.Println(n,s)
s2:=make([]int,6) //在不数组间复制
n=copy(s2,s)
fmt.println(n,s2)
}
输出:
3 [0 1 2 3 5 6 7 7 8 9]
6 [0 1 2 3 5 6]
还可直接从字符串中复制数据到[]byte
func main(){
b:=make([]byte,3)
n:=copy(b,"abcde")
fmt.println(n,b)
}
输出:
3 [97 98 99]
字典(哈希表)是一种使用频率极高的数据结构。
作为无序键值对集合,字典要求key必须是支持相等运算符(== ,!=)的数据类型。比如,数字、字符串、指针
数组、结构体,以及对应接口类型。
字典是引用类型,使用make函数或初始化表达语句来创建。
func main(){
m:=make(map[string]int)
m["a"]=1
m["b"]=2
m2:=map[int]struct{ //值为匿名结构体类型
x int
}{
1: {x:100}, //可省略key,value类型标签
2: {x:200},
}
fmt.println(m,m2)
}
访问不存在的键值,默认返回零值,不会引发错误。但推荐使用ok-idiom模式,毕竟通过零值无法判断键值是否存在,或许存储的value本就是零。
func main(){
m:=map [string]int{
"a":1,
"b":2,
}
m["a"]=10
m["c"]=20
if v,ok:=m["d"];ok{ //使用ok-idiom判断key是否存在,返回值
println(v)
}
delete(m,"d") //删除键值对。不存在时,不会出错
}
map是无序的,对字典进行迭代,每次返回的键值次序都不同。
函数len返回当前键值对数量,cap不接受字典类型。字典是“not addressable",故不能直接修改value成员(结构或数组)。
func main(){
type user struct{
name string
age byte
}
m:=map[int]sting{
1:{"tom",19},
}
m[1].age +=1 //错误
}
正确做法是返回整个value,待修改后再设置字典键值,会直接用指针类型。
type user struct{
name string
age byte
}
func main(){
m:=map[int]user{
1:{"tom",19},
}
u:=m[1]
u.age +=1
m[1] =u
m2:=map[int]*user{ //value是指针类型
1:&user{"jak",20}
}
m2[1].age++ //返回的是指针,可透过指针修改目标对象
}
不能对nil字典进行写操作,但能读
var m1 map[int]string //只是声明一个map,没有初始化, 为空(nil)map
fmt.Println(m1 == nil) //true
//m1[1] = "Luffy" //nil的map不能使用err, panic: assignment to entry in nil map
m4 := make(map[int]string, 10) //第2个参数指定容量
内存地址是内存中每个字节单元的唯一编号,而指针则是一个实体。指针会分配内存空间,相当于一个专门用来保存地址的整型变量。
指针运算为左值时,我们可更新目标对象状态,而为右值时则为了获取目标状态。
func main(){ x:=10 var p *int =&x //取地址,保存到指针变量 *p +=20 //用指针间接引用,并更新对象 println(p,*p) }
并非所有对象都能进行取地址操作
m:=map[string]int{"a":1} println(&m["a"]) //错误
指针类型支持相等运算符,但不能做加减法运算和类型转换。
可通过unsafe.Pointer将指针转换为uintptr后进行加减法运算,但可能会造成非法访问。
Go语言保留了指针,但与C语言指针有所不同。主要体现在:
默认值 nil
操作符 “&” 取变量地址, “*” 通过指针访问目标对象
不支持指针运算,不支持 “->” 运算符,直接⽤ “.” 访问目标成员
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址:
指向指针的指针变量声明格式如下:
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 */
}
以上实例允许输出结果为:
交换前 a 的值 : 100
交换前 b 的值 : 200
交换后 a 的值 : 200
交换后 b 的值 : 100
var mat MaterialInfo
err := json.Unmarshal([]byte(args[0]), &mat)
if err != nil {
return shim.Error("反序列化信息时发生错误")
}
[]byte(args[0]) 字符串切片,将arg[0]中的字符串存储在切片中
json.Unmarshall 解析json字符串
type Stu struct {
Name string `json:"name"`
Age int
HIgh bool
sex string
Class *Class `json:"class"`
}
type Class struct {
Name string
Grade int
}
func main() {
//实例化一个数据结构,用于生成json字符串
stu := Stu{
Name: "张三",
Age: 18,
HIgh: true,
sex: "男",
}
//指针变量
cla := new(Class)
cla.Name = "1班"
cla.Grade = 3
stu.Class=cla
//Marshal失败时err!=nil
jsonStu, errs := json.Marshal(stu)
if errs != nil {
fmt.Println("生成json字符串错误")
}
//jsonStu是[]byte类型,转化成string类型便于查看
fmt.Println(string(jsonStu))
data:="{\"name\":\"张三\",\"Age\":18,\"high\":true,\"sex\":\"男\",\"CLASS\":{\"naME\":\"1班\",\"GradE\":3}}"
str:=[]byte(data)
//1.Unmarshal的第一个参数是json字符串,第二个参数是接受json解析的数据结构。
//第二个参数必须是指针,否则无法接收解析的数据,如stu仍为空对象StuRead{}
//2.可以直接stu:=new(StuRead),此时的stu自身就是指针
stus:=Stu{}
err:= json.Unmarshal(str, &stus)
if err!=nil{
fmt.Println(err)
}
fmt.Println(stu)
fmt.Println(stu.Age)
fmt.Println(stu.Class)
}
{"name":"张三","Age":18,"HIgh":true,"class":{"Name":"1班","Grade":3}}
{张三 18 true 男 0xc0000a6018}
18
&{1班 3}
type StuRead struct {
Name interface{} `json:"name"`
Age interface{}
HIgh interface{}
sex interface{}
Class interface{} `json:"class"` //interface{} 类型,空接口
}
func main() {
data:="{\"name\":\"张三\",\"Age\":18,\"HIgh\":true,\"sex\":\"男\",\"class\":{\"Name\":\"1班\",\"Grade\":3}}"
str:=[]byte(data)
stu:=StuRead{}
err:=json.Unmarshal(str,&stu)
if err!=nil{
fmt.Println(err)
}
fmt.Println(stu)
fmt.Println(stu.Age)
}
{张三 18 true <nil> map[Grade:3 Name:1班]}
18