Go 语言类型转换
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。由于Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明:
valueOfTypeB = typeB(valueOfTypeA)
a := 5.0
b := int(a)
只有相同底层类型的变量之间可以进行相互转换(如将 int16 类型转换成 int32 类型),不同底层类型的变量相互转换时会引发编译错误(如将 bool 类型转换为 int 类型)
strconv
Go语言中strconv包实现了基本数据类型和其字符串表示的相互转换。
string与int类型转换
Atoi()
Atoi()函数用于将字符串类型的整数转换为int类型
func Atoi(s string) (i int, err error)
s1 := "100"
i1, err := strconv.Atoi(s1)
if err != nil {
fmt.Println("can't convert to int")
} else {
fmt.Printf("type:%T value:%#v\n", i1, i1)
//type:int value:100
}
Itoa()
Itoa()函数用于将int类型数据转换为对应的字符串表示
func Itoa(i int) string
i2 := 200
s2 := strconv.Itoa(i2)
fmt.Printf("type:%T value:%#v\n", s2, s2)
//type:string value:"200"
a的典故
C语言中没有string类型而是用字符数组(array)表示字符串,所以Itoa对很多C系的程序员很好理解。
Parse系列函数
Parse类函数用于转换字符串为给定类型的值:ParseBool()、ParseFloat()、ParseInt()、ParseUint()。
ParseBool()
func ParseBool(str string) (value bool, err error)
fmt.Print(strconv.ParseBool("T"))
// true
返回字符串表示的bool值。它接受1、0、t、f、T、F、true、false、True、False、TRUE、FALSE;否则返回错误。
ParseInt()
func ParseInt(s string, base int, bitSize int) (i int64, err error)
res,_:=strconv.ParseInt("-32",0,0)
fmt.Printf("%v %T", res, res)
// -32 int64
返回字符串表示的整数值,接受正负号。
base指定进制(2到36),如果base为0,则会从字符串前置判断,”0x”是16进制,”0”是8进制,否则是10进制;
bitSize指定结果的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64;
ParseUnit()
func ParseUint(s string, base int, bitSize int) (n uint64, err error)
ParseUint类似ParseInt但不接受正负号,用于无符号整型。
ParseFloat()
func ParseFloat(s string, bitSize int) (f float64, err error)
解析一个表示浮点数的字符串并返回其值。
如果s合乎语法规则,函数会返回最为接近s表示值的一个浮点数(使用IEEE754规范舍入)。
bitSize指定了期望的接收类型,32是float32(返回值可以不改变精确值的赋值给float32),64是float64;
Format系列函数
Format系列函数实现了将给定类型数据格式化为string类型数据的功能。
FormatBool()
func FormatBool(b bool) string
根据b的值返回”true”或”false”。
FormatInt()
func FormatInt(i int64, base int) string
res:=strconv.FormatInt(12,2)
fmt.Println(res,reflect.TypeOf(res))
// 1100 string
返回i的base进制的字符串表示。base 必须在2到36之间,结果中会使用小写字母’a’到’z’表示大于10的数字。
FormatUint()
func FormatUint(i uint64, base int) string
是FormatInt的无符号整数版本。
FormatFloat()
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
函数将浮点数表示为字符串并返回。
bitSize表示f的来源类型(32:float32、64:float64)
fmt表示格式:’f’(-ddd.dddd)、’b’(-ddddp±ddd,指数为二进制)、’e’(-d.dddde±dd,十进制指数)、’E’(-d.ddddE±dd,十进制指数)、’g’(指数很大时用’e’格式,否则’f’格式)、’G’(指数很大时用’E’格式,否则’f’格式)。
prec控制精度(排除指数部分):对’f’、’e’、’E’,它表示小数点后的数字个数;对’g’、’G’,它控制总的数字个数。
go内置函数和init的导包顺序
内置函数
append -- 用来追加元素到数组、slice中,返回修改后的数组、slice
close -- 主要用来关闭channel
delete -- 从map中删除key对应的value
panic -- 停止常规的goroutine (panic和recover:用来做错误处理)
recover -- 允许程序定义goroutine的panic动作
real -- 返回complex的实部 (complex、real imag:用于创建和操作复数)
imag -- 返回complex的虚部
make -- 用来分配内存,返回Type本身(只能应用于slice, map, channel)
new -- 用来分配内存,主要用来分配值类型,比如int、struct。返回指向Type的指针
cap -- capacity是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
copy -- 用于复制和连接slice,返回复制的数目
len -- 来求长度,比如string、array、slice、map、channel ,返回长度
print、println -- 底层打印函数,在部署环境中建议使用 fmt 包
Init函数和main函数
init函数
-
init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
-
每个包可以拥有多个init函数
-
包的每个源文件也可以拥有多个init函数
-
同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)
-
不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序
-
init函数不能被其他函数调用,而是在main函数执行之前,自动被调用
init函数和main函数的异同
相同点:
1. 两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
不同点:
1. init可以应用于任意包中,且可以重复定义多个。
2. main函数只能用于main包中,且只能定义一个。
在Go语言中,执行顺序为:初始化变量–》init函数–》main函数
两个函数的执行顺序:
对同一个go文件的init()调用顺序是从上到下的。
对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。
对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。
init函数的作用
1)初始化不能采用初始化表达式初始化的变量。
2)程序运行前的注册。
3)实现sync.Once功能。
下划线_
在import中
- 当导入一个包时,该包下的文件里所有init()函数都会被执行,然而,有些时候我们并不需要把整个包都导入进来,仅仅是是希望它执行init()函数而已。这个时候就可以使用 import 引用该包。
- 即使用【import _ 包路径】只是引用该包,仅仅是为了调用init()函数,所以无法通过包名来调用包中的其他函数。
- 如:
import _ "github.com/go-sql-driver/mysql"
其他:略
基本类型
布尔值
-
布尔类型变量的默认值为false。
-
Go 语言中不允许将整型强制转换为布尔型.
-
布尔型无法参与数值运算,也无法与其他类型进行转换。
多行字符串
反引号间的换行将被作为字符串中的换行,但是所有的转义字符均无效,文本将会原样输出。
byte和rune类型
uint8类型,或者叫 byte 型,代表了ASCII码的一个字符。
rune类型,代表一个 UTF-8字符。
// 遍历字符串
func traversalString() {
s := "pprof.cn博客"
//byte
for i := 0; i < len(s); i++ {
fmt.Printf("%v(%c) ", s[i], s[i])
}
fmt.Println()
//rune
for _, r := range s {
fmt.Printf("%v(%c) ", r, r)
}
fmt.Println()
}
--------------------------输出
112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174(®) 162(¢)
112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338(博) 23458(客)
- 字符串底层是一个byte数组,所以可以和[]byte类型相互转换。字符串是不能修改的
- 字符串是由byte字节组成,所以字符串的长度是byte字节的长度。
- rune类型用来表示utf8字符,一个rune字符由一个或多个byte组成。
修改字符串
要修改字符串,需要先将其转换成[]rune或[]byte,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组。
func changeString() {
s1 := "hello"
// 强制类型转换
byteS1 := []byte(s1)
byteS1[0] = 'H'
fmt.Println(string(byteS1))
s2 := "博客"
runeS2 := []rune(s2)
runeS2[0] = '狗'
fmt.Println(string(runeS2))
}
go new/make/map/struct
new与make的区别
1.二者都是用来做内存分配的。
2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
map
判断某个键是否存在
Go语言中有个判断map中键是否存在的特殊写法,格式如下:
value, ok := map[key]
hash冲突
开放定址(线性探测)和拉链的优缺点
-
拉链法比线性探测处理简单
-
线性探测查找是会被拉链法会更消耗时间
-
线性探测会更加容易导致扩容,而拉链不会
-
拉链存储了指针,所以空间上会比线性探测占用多一点
-
拉链是动态申请存储空间的,所以更适合链长不确定的
Go中Map的实现原理
map同样也是数组存储的的:
- 每个数组下标处存储的是一个bucket
- 每个bucket中可以存储8个kv键值对
- 当每个bucket存储的kv对到达8个之后,会通过overflow指针指向一个新的bucket,从而形成一个链表
当往map中存储一个kv对时,通过k获取hash值,hash值的低八位和bucket数组长度取余,定位到在数组中的那个下标,hash值的高八位存储在bucket中的tophash中,用来快速判断key是否存在,key和value的具体值则通过指针运算存储,当一个bucket满时,通过overfolw指针链接到下一个bucket。
结构体
Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
自定义类型
//将MyInt定义为int类型
type MyInt int
通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
类型别名
type TypeAlias = Type
类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型
类型定义和类型别名的区别
//类型定义
type NewInt int
//类型别名
type MyInt = int
func main() {
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var关键字声明结构体类型。
匿名结构体
package main
import (
"fmt"
)
func main() {
var user struct{Name string; Age int}
user.Name = "pprof.cn"
user.Age = 18
fmt.Printf("%#v\n", user)
}
创建指针类型结构体
Go语言中支持对结构体指针直接使用.来访问结构体的成员。
什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
任意类型添加方法
//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
结构体字段的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
json序列化,反序列化
//Student 学生
type Student struct {
ID int
Gender string
Name string
}
//Class 班级
type Class struct {
Title string
Students []*Student
}
func main() {
c := &Class{
Title: "101",
Students: make([]*Student, 0, 200),
}
for i := 0; i < 10; i++ {
stu := &Student{
Name: fmt.Sprintf("stu%02d", i),
Gender: "男",
ID: i,
}
c.Students = append(c.Students, stu)
}
//JSON序列化:结构体-->JSON格式的字符串
data, err := json.Marshal(c)
if err != nil {
fmt.Println("json marshal failed")
return
}
fmt.Printf("json:%s\n", data)
//JSON反序列化:JSON格式的字符串-->结构体
str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"},{"ID":2,"Gender":"男","Name":"stu02"},{"ID":3,"Gender":"男","Name":"stu03"},{"ID":4,"Gender":"男","Name":"stu04"},{"ID":5,"Gender":"男","Name":"stu05"},{"ID":6,"Gender":"男","Name":"stu06"},{"ID":7,"Gender":"男","Name":"stu07"},{"ID":8,"Gender":"男","Name":"stu08"},{"ID":9,"Gender":"男","Name":"stu09"}]}`
c1 := &Class{}
err = json.Unmarshal([]byte(str), c1)
if err != nil {
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
结构体标签(Tag)
Tag是结构体的元信息,可以在运行的时候通过反射的机制读取出来。
Tag在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:
key1:"value1" key2:"value2"
//Student 学生
type Student struct {
ID int `json:"id"` //通过指定tag实现json序列化该字段时的key
Gender string //json序列化是默认使用字段名作为key
name string //私有不能被json包访问
}
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address Address `json:"address"`
}
type Address struct {
Country string `json:"country"`
City string `json:"city"`
}
func NewPerson(name string,age int,country,city string) *Person{
return &Person{
Name: name,
Age: age,
Address: Address{
Country: country,
City: city,
},
}
}
func main() {
p1:=NewPerson("12121",21,"313","1313")
res1, _ :=json.Marshal(p1)
fmt.Printf("%v\n%s\n%#v\n",res1,res1,res1)
test :=`{"name":"123","age":21,"address":{"country":"china","city":"31313"}}`
res2:=new(Person)
err := json.Unmarshal([]byte(test), res2)
if err != nil {
fmt.Println(err)
return
}
fmt.Println(res2.Address)
}
go 类型断言
switch 语句
switch k {
case 0:
println("fallthrough")
fallthrough
/*
Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;
而如果switch没有表达式,它会匹配true。
Go里面switch默认相当于每个case最后带有break,
匹配成功后不会自动向下执行其他case,而是跳出整个switch,
但是可以使用fallthrough强制执行后面的case代码。
*/
case 1:
fmt.Println("1")
case 2:
fmt.Println("2")
default:
fmt.Println("def")
}
类型断言(Type Assertion)
类型断言(Type Assertion)是一个使用在接口值上的操作,用于检查接口类型变量所持有的值是否实现了期望的接口或者具体的类型。
在Go语言中类型断言的语法格式如下:
value, ok := x.(T)
其中,x 表示一个接口的类型,T 表示一个具体的类型(也可为接口类型)。
该断言表达式会返回 x 的值(也就是 value)和一个布尔值(也就是 ok),可根据该布尔值判断 x 是否为 T 类型:
func main() {
var x interface{}
switch i := x.(type) { // 带初始化语句
case nil:
fmt.Printf(" x 的类型 :%T\r\n", 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("未知型")
}
接口值
空接口可以存储任意类型的值,那我们如何获取其存储的具体数据呢?
一个接口的值(简称接口值)是由一个具体类型和具体类型的值两部分组成的。这两部分分别称为接口的动态类型和动态值。
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
想要判断空接口中的值这个时候就可以使用类型断言,其语法格式:
x.(T)
其中:
x:表示类型为interface{}的变量
T:表示断言x可能是的类型。
该语法返回两个参数,第一个参数是x转化为T类型后的变量,第二个值是一个布尔值,若为true则表示断言成功,为false则表示断言失败。
select
select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
超时判断
var resChan = make(chan int)
// do request
func test() {
select {
case data := <-resChan:
doData(data)
case <-time.After(time.Second * 3):
fmt.Println("request time out")
}
}
go 闭包,延迟调用
go函数
• 无需声明原型。
• 支持不定 变参。
• 支持多返回值。
• 支持命名返回参数。
• 支持匿名函数和闭包。
• 函数也是一种类型,一个函数可以赋值给变量。
• 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
• 不支持 重载 (overload)
• 不支持 默认参数 (default parameter)。
传参
在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。
注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。
注意2:map、slice、chan、指针、interface默认以引用的方式传递。
不定参数
不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。(可变参数)
Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。
在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。
func myfunc(args ...int) { //0个或多个参数
}
func add(a int, args…int) int { //1个或多个参数
}
func add(a int, b int, args…int) int { //2个或多个参数
}
注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.
闭包、递归
闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。
func test() func() {
x := 100
fmt.Printf("x (%p) = %d\n", &x, x)
return func() {
fmt.Printf("x (%p) = %d\n", &x, x)
}
}
func main() {
test()()
}
在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可。
外部引用函数参数局部变量
func add(base int) func(int) int {
return func(i int) int {
base += i
return base
}
}
func main() {
tmp1 := add(10)
// 0x7cbda0 func(int) int
fmt.Printf("%v %T \n",tmp1,tmp1)
fmt.Println(tmp1(1))
fmt.Printf("%v %T \n",tmp1,tmp1)
fmt.Println(tmp1(2))
tmp2 := add(100)
fmt.Printf("%v %T \n",tmp1,tmp1)
fmt.Println(tmp2(1))
fmt.Printf("%v %T \n",tmp1,tmp1)
fmt.Println(tmp2(2))
}
------------------------------------
// base一直存在,不断被修改
0x10bbf20 func(int) int
11
0x10bbf20 func(int) int
13
0x10bbf20 func(int) int
101
0x10bbf20 func(int) int
103
延迟调用(defer)
defer 是先进后出队列,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。
defer 碰上闭包
func main() {
var whatever [5]struct{}
for i := range whatever {
defer func() { fmt.Println(i) }()
}
}
------------------------------
4
4
4
4
4
Each time a “defer” statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.
也就是说函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.
defer后面的语句在执行的时候,函数调用的参数会被保存起来,但是不执行。也就是复制了一份。
多个defer
多个 defer 注册,按 FILO 次序执行 ( 先进后出 )。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
package main
func test(x int) {
defer println("a")
defer println("b")
defer func() {
println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
}()
defer println("c")
}
func main() {
test(0)
}
-----------------------------------------
c
b
a
panic: runtime error: integer divide by zero
defer和return
go的return语句不是原子性的
func foo() (i int) {
i = 0
defer func() {
fmt.Println(i)
}()
return 2
}
func main() {
foo()
}
-----------------------------------
2
return 2
相当于 i = 2- 执行defer语句
fmt.Println(i)
return i
defer nil 函数
func test() {
var run func() = nil
// 相当于 nil()
defer run()
fmt.Println("runs")
}
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
test()
}
-------------------------------------
runs
runtime error: invalid memory address or nil pointer dereference
名为 test 的函数一直运行至结束,然后 defer 函数会被执行且会因为值为 nil 而产生 panic 异常。
go 异常捕获和处理(panic/recover)
异常处理
Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
panic
- 内置函数
- 假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
- 返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
- 直到goroutine整个退出,并报告错误
recover
- 内置函数
- 用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
- 一般的调用建议
a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
b). 可以获取通过panic传递的error
使用
1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去
3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
func main() {
test()
}
func test() {
defer func() {
if err := recover(); err != nil {
println(err.(string)) // 将 interface{} 转型为具体类型。
}
}()
panic("panic error!")
}
-------------------------------------
panic error!
延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。
func test() {
defer func() {
fmt.Println(recover())
}()
defer func() {
panic("defer panic")
}()
panic("test panic")
}
func main() {
test()
}
---------------------------------
defer panic
实例
捕获函数 recover 只有在延迟调用内直接调用才会终止错误,否则总是返回 nil。任何未捕获的错误都会沿调用堆栈向外传递。
func test() {
defer func() {
fmt.Println(recover()) //有效 打印正确异常
}()
defer recover() //无效!
defer fmt.Println(recover()) //无效! 打印nil
defer func() {
func() {
println("defer inner") // 打印defer inner
recover() //无效!
}()
}()
panic("test panic")
}
func main() {
test()
}
-------------------------------------
defer inner
<nil>
test panic
如何区别使用 panic 和 error 两种方式
惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。