Go基础
Go基础
提示
多条语句能不能写一行
- 可以,如果语句后面加上分号再写下一条语句,都写在一行是可以进行编译的,而且一行一条完整的语句结尾写不写分号都可以编译
关于本文中、其他书籍、官网中的T类型
- T指代所有的基础类型,不是基本类型,也不是引用类型
- 包含了除指针和接口外的其他类型
简介
特点
- 清晰并且简洁:Go 努力保持小并且优美,你可以在短短几行代码里做许多事情
- 并行:Go 让函数很容易成为非常轻量的线程。这些线程在Go 中被叫做goroutines
- Channel:这些goroutines 之间的通讯由channel完成
- 快速:编译很快,执行也很快。目标是跟C 一样快。编译时间用秒计算
- 安全:当转换一个类型到另一个类型的时候需要显式的转换并遵循严格的规则。Go 有垃圾收集,在Go 中无须free(),语言会处理这一切
- 标准格式化:Go 程序可以被格式化为程序员希望的(几乎)任何形式,但是官方格式是存在的。标准也非常简单:gofmt 的输出就是官方认可的格式;
- 类型后置:类型在变量名的后面,像这样var a int,来代替C 中的int a;
- UTF-8:任何地方都是UTF-8 的,包括字符串以及程序代码。你可以在代码中使用
φ = φ + 1 - 开源:Go 的许可证是完全开源的,参阅Go 发布的源码中的LICENSE 文件
- 开心:用Go 写程序会非常开心!
引用 Learning as we Go一书
hello go
package main // 必须的,声明包名
import "fmt" // 非必须的,导包
func main(){
fmt.Println("hello go")
}
执行
go build xxx.go ## 编译
./xxx.exe ## 运行 Linux下 ./xxx
go run xxx.go ##
变量
标识符
命名规则和其他语言相同
包名:保持 package 名字和目录一致,不和标准库冲突
声明与赋值
提示
- 引入的包不使用会报错
- 声明的变量不使用会报错
- 短变量只能用于声明局部变量
- 全局跨包变量名首字母大写即可
var b bool = false // 声明并赋值
var b = false // 类型推断:声明并赋值
a := 15 // :声明并赋值
var x, y int // 相同类型
a, b := 20, 16 // 平行赋值
var ( // 成组声明,const 和import 同样允许这么做,注意是小括号
x int
b bool
)
示例
package main
import "fmt"
// 全局变量
var n1 = 100
var n2 = 20.12
var (
n3 = 500
n4 = 34.12
)
func main(){
// 局部变量
var age int // 先声明
age = 18 // 再赋值
var num int = 22 // 声明并赋值
var num1 int // 只声明,不赋值,有默认值
var num2 = "tom" // 没有定义类型,自动推断
sex := "nan" // 省略var
// 多变量
var a,b,c int // 声明
var m,n,l = 10,"ha",1.9 // 声明并赋值
m1,n1,l1 := 20,"la",2.9 // 简写
m,n,l = 30,"ba",3.9 // 多变量赋值
fmt.Println(age)
fmt.Println(num,num1,num2)
fmt.Println(sex)
fmt.Println(a,b,c)
fmt.Println(m1,n1,l1)
fmt.Println(m,n,l)
}
基本类型
提示
与其他语言差距还是挺大的
- 基本类型(值类型):int系列、float系列、bool、string、数组、结构体
- 变量直接存储值,内存通常在栈中分配
- 引用类型:指针、slice切片、管道channel、接口interface、map、函数等
- 变量存储的是一个地址,这个地址对应的空间才是真正存储数据 (值), 内存通常在堆上分配,当没有任何变量引用这个地址时,改地址对应的数据空间就成为了一个垃圾,由 GC 来回收
go体系 --- 字节数 --- java体系 --- 无符号 --- go体系其他
int -----------32 位硬件是 int32 ,64 位上是 int64 --------- uint
int8 ----------- 1字节 ------ byte ---- uint8 ------- 与byte等价
int16 ---------- 2字节 ------ short ---- uint16
int32 ---------- 4字节 -------- int ----- uint32 ------ rune与int32等价,多用于字符
int64 ---------- 8字节 ------- long ---- uint64
float32 -------- 4字节 ------- float
float64 -------- 8字节 ------- double
byte ------------ 1字节 ------ byte --------------------- 与uint8等价
bool ------------ 1字节 ------- boolean
string ----------- go中属于基本数据类型,也是不可变的
提示
复数:go原生支持复数,complex128,complex64,complex32,这里不赘述了
错误:var e error 定义了一个error 类型的变量e,默认值是nil,在接口详细说明
编码:Golang中没有那么多编码格式,统一都是utf8,查询字符utf8码值
字符:Golang 中没有专门的字符类型,如果要存储单个字符(字母),一般使用byte来保存
- 直接使用就是一个整数,如
var a = 'a',输出的就是 97, - 既然是整数就当然可以运算,
‘a’+10,输出就是 107 - 说白了go中的字符本质就是数字
- 字符用
%c格式化
- 直接使用就是一个整数,如
字符串就是[]byte,ASCII码可以用byte接收(0-255),汉字就不行,字符用什么接收就一个原则,盛得下就行,如
a 用int8就行
山 就得用 int16
隆 就得用 int32
基本类似默认值:都是0类值(0,false,“”,nil等)
类型推断时,整型默认为int型,浮点型默认为float64
查看字节数 --- unsafe.Sizeof(num)
package main
import(
"fmt"
"unsafe"
)
func main(){
var num1 int8 = 120
fmt.Println(unsafe.Sizeof(num1)) // 1
num2:=120
fmt.Printf("类型为:%T",num2) // int 整型默认为int型
fmt.Println()
fmt.Println(unsafe.Sizeof(num2)) // 8 64位操作系统int等价于int64
num3:=3.14E+2 // 类似科学计数法
fmt.Printf("类型为:%T\n",num3) // float64
fmt.Println(num3) // 314
c1:='n'
var c2 byte = 'm'
fmt.Println(c1) // 直接输出显示编码
fmt.Printf("c1类型是:%T\n",c1) // 默认类型为int32
fmt.Printf("c1是:%c\n",c1) // 显示字符必须格式化输出
fmt.Println(c2) // 显示编码
fmt.Printf("c2类型是:%T\n",c2) // 定义的类型为uint8 等价于 byte
fmt.Printf("c2是:%c\n",c2) // 显示字符必须格式化输出
c3:="bbbbb" + "ddddd"
c3+="ccccc"
c3 = c3 + "eeeee" + // 拼接换行时 + 号放行末, 否则不知道在那结束会报错
"fffff"
fmt.Printf("c3是:%s\n",c3) // 格式化输出
fmt.Println(c3) // 可以直接输出
}
类型转换
| from/to | b []byte | i []int | r []rune | s string | f float32 | i int |
|---|---|---|---|---|---|---|
| []byte | x | []byte(s) | ||||
| []int | x | []int(s) | ||||
| []rune | x | []rune(s) | ||||
| string | string(b) | string(i) | string(r) | x | ||
| float32 | x | float32(i) | ||||
| int | int(f) | x |
注意
go中没有隐式转换,无论大转小,还是小转大,只能强转
float f = 1.1f;
double a = f + 10;
上述java代码完全没问题
但是,go中不行
var f float32
var d float64
f = 1.1
d = f + 10 // 报错 Cannot use 'f + 10' (type float32) as the type float64
强制转换 --- 类型(变量) ---- 而java中(类型)变量
var f float32
var d float64
f = 1.1
d = float64(f) + 10 // 强转
基本数据类型转string
- 方式一:
fmt.Sprintf("格式",变量)- si := fmt.Sprintf("%d",i)
- sf := fmt.Sprintf("%f",f)
- sb := fmt.Sprintf("%t",b)
- sc := fmt.Sprintf("%c",c)
- 方式二:
strconv.FormatXxx- si1 := strconv.FormatInt(int64(i),10) // 强转为int64,进制
- sf1 := strconv.FormatFloat(f,'f',9,64) // 待转变量,格式,保留小数位,float64还是32
- sb1 := strconv.FormatBool(b)
string转基本数据类型:strconv.ParseXxx
- i,_ := strconv.ParseInt(si,10,64) // 由于i声明时默认用int接收,string转int64与i匹配不上,重新声明新变量i
- f,_ = strconv.ParseFloat(sf,64)
- b,_ = strconv.ParseBool(sb)
- 转不了的会用默认值,比如 “hello” 转 int ,结果就是0
- 转字符数组
chars := []rune(str),rune是int32的别名
示例代码
package main
import (
"fmt"
"strconv"
)
func main(){
// 转string方式一:推荐
i := 18
f := 19.9
b := false
c := 'a'
si := fmt.Sprintf("%d",i)
sf := fmt.Sprintf("%f",f)
sb := fmt.Sprintf("%t",b)
sc := fmt.Sprintf("%c",c)
fmt.Println(si)
fmt.Println(sf)
fmt.Println(sb)
fmt.Println(sc)
fmt.Println()
// 转string方式二
si1 := strconv.FormatInt(int64(i),10) // 强转为int64,进制
sf1 := strconv.FormatFloat(f,'f',9,64) // 待转变量,格式,保留小数位,float64还是32
sb1 := strconv.FormatBool(b)
fmt.Println(si1)
fmt.Println(sf1)
fmt.Println(sb1)
fmt.Println()
// 转基本数据类型
i1,_ := strconv.ParseInt(si,10,64) // 由于i声明时默认用int接收,string转int64与i匹配不上,重新声明新变量i1
f,_ = strconv.ParseFloat(sf,64)
b,_ = strconv.ParseBool(sb)
fmt.Println(i1)
fmt.Println(f)
fmt.Println(b)
}
常量
不赋值会默认用上个常量的值
const (
A = "j" // j
B // j
C // j
)
神奇的iota
自增
- 不同 const 定义块互不干扰
- 所有注释行和空行全部忽略
- 从第 1 行开始,iota 从 0 逐行加 1
_,跳过一个数- 每次显式使用iota都会恢复计数
- 不显示使用,会在上一行基础上递增
示例1:
const (
zero int = iota // iota = 0
one // iota = 1
three = iota + 1 // iota = 2,three = 2+1
four // iota = 3,但是没有显式调用,在上一行常量基础上自增
five = iota + 1 // iota = 4,iota = 4+1
)
// 3 4 5 why not 3 4 6
fmt.Println(three, four, five)
示例2:
const (
Apple, Banana = iota + 1, iota + 100 // 1,100
Cherimoya, Durian // 2,101
Elderberry, Fig // 3,102
)
示例3
const (
a = iota //0
b //1
c //2
d = "ha" //独立值"ha" iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
递减
- 加上负号就开始递减
示例1:
const (
a = -iota // 0
b // -1
c // -2
)
示例2:源码中
const (
RFP = -(iota + 1)
RSB
RSP
RPC
)
示例3:
const (
zero int = iota // 0
one // 1
three = -iota + 1 // -1 = -2+1
four // -2
five = iota + 1 // 5 = 4+1
)
有趣的运算
const (
i=1<<iota // 1 = 1*2^0
j=3<<iota // 6 = 3*2^1
k // 12 = 3*2^2
l // 24 = 3*2^3
)
运算符
和其他语言基本相同
注意
没有三元运算符
i++、i--只能独立使用, 没有++i、--i
a = i++ // x
a = i-- // x
if i++ > 0 {} // x
流程控制
条件
if
if ---- 括号不用写(也可以写),必须有{},可以在条件判断时定义变量:经常用于错误的处理
if age:=20;age>18{ }
if err := Chmod(0664); err != nil {
fmt.Printf(err)
return err
}
else不能换行
var x int = 4
if x>2{
fmt.Println("v")
}
else{
fmt.Println("x")
}
switch
提示
和其他语言差距还是很大的,Go 的switch 非常灵活。表达式不必是常量或整数,而如果switch 没有表达式,它会匹配true 。这产生一种可能——使用switch编写if-else-if-else 判断序列
不能使用break关键字,天生break,穿透一个case(反break)用 fallthrough
多个值用逗号隔开,相当于java8中摞着写(本质是穿透到了下一个)
语法上也可以摞着写,但是匹配到的不会有作用,因为没有语句执行就返回了
case 可以是
- 常量 ---- 不能重复
- 变量,有返回值的函数 ---- 需要保证类型一致
switch后可以啥也没有,当if使用
switch后也可以声明变量
type switch 类似java中的 instanceof
示例1:fallthrough
package main
import "fmt"
func main(){
score := 83
switch score/10 {
case 12,11,10: // 多个值用逗号隔开
fmt.Println("优秀")
case 9,8,7:
fmt.Println("不错")
fallthrough // 本来只执行“不错”,fallthrough下穿到“及格”
case 6:
fmt.Println("及格")
case 5,4,3,2,1,0:
fmt.Println("差劲")
default:
fmt.Println("输入有误")
}
}
示例2:case是变量
var n1 int8 = 20
var n2 int16 = 20
switch n1 {
case n2: // 类型不一致
fmt.Println("v")
default:
fmt.Println('x')
}
示例3:当if用
switch{
case a==1:
fmt.Println(1)
case a==2:
fmt.Println(2)
default:
fmt.Println("x")
}
示例4:switch后也可以声明变量
switch a:=10;{
case a>0:
fmt.Println("v")
default:
fmt.Println("x")
}
示例5:type switch
var x interface{}
y := 10.0
x = y
switch i := x.(type) {
case nil:
fmt.Println("x类型为:%T",i)
case int:
fmt.Println("x类型为 int")
case func(int) float64:
fmt.Println("x类型为 func(int) float64")
case bool,string:
fmt.Println("x类型为 bool 或 string")
default:
fmt.Println("不知道")
}
循环
提示
没有while 、do...while
for
循环结构中有break、continue,同样有标签,还有goto,但一般不用
for ---- 可以省略括号,变量声明不能用var,只能用 :=
死循环
;;可省略,配合break使用
for ;; { // ;;可省略
}
for{
}
平行赋值
// 反转数组
a := []int{1, 2, 3, 4, 5}
for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 {
a[i], a[j] = a[j], a[i] // 平行赋值
}
for-range
键值对循环 ---- for-range
package main
import "fmt"
func main(){
str := "hello golang 你好啊!"
for key ,value := range str {
fmt.Printf("%d位置上的字符为 %c\n",key,value)
}
/*
当人们讨论字符时,多数是指英文的 8位字符。
UTF-8 字符可能会有32 位,称作rune。
在这个例子里,value 的类型是rune。
*/
}
array、slice、map
这三个是引用类型
数组
提示
和java区别挺大
像
var arr = [10]int这样的数组类型有固定的大小。大小是类型的一部分。由于不同的大小是不同的类型,因此不能改变大小数组同样是值类型的:将一个数组赋值给另一个数组,会复制所有的元素。尤其是当向函数内传递一个数组的时候,它会获得一个数组的副本,而不是数组的指针。
建
- 普通声明:
var arr [5]int---- 被初始化为零值 - 复合声明:
a := [3]int{1, 2, 3}也可以简写为a := [...]int{1, 2, 3},Go 会自动统计元素的个数 - 二维数组
a := [3][2]int{ [2]int{1,2}, [2]int{3,4}, [2]int{5,6} }- 可简写为
a := [3][2]int{ [...]int{1,2}, [...]int{3,4}, [...]int{5,6} } - 进一步简写
a := [3][2]int{ {1,2}, {3,4}, {5,6} }
- 可简写为
- 普通声明:
改:
arr[1] = 2不能增删
package main
import (
"fmt"
)
func main(){
var arr [3]int // 先声明,后赋值
arr[2] = 3
fmt.Println(arr)
scores := [2]int{1,2} // 声明并赋值
arr2 := [3]string{1:"a",2:"哈"}
var arr3 = [2]byte{'a','b'}
arr4 := [...]float32{11.1,2}
fmt.Println(scores)
fmt.Println(arr2) // [ a 哈]
fmt.Println(arr3)
fmt.Println(arr4)
fmt.Printf("类型为 %T \n",arr4) // 类型为 [2]float32, 长度属于类型的一部分
fmt.Println("----------")
for _,v := range arr2{
fmt.Println(v)
}
fmt.Println("----------")
change(arr)
fmt.Println(arr[2]) // 传值不传址
// 二维数组
arr5 := [2][3]int{{1,2,3},{4,5,6}}
fmt.Println(arr5)
}
func change(arr [3]int){
arr[2] = 5
}
public class TestArr {
public static void main(String[] args) {
int[] arr = { 1, 2, 3 };
change(arr);
System.out.println(arr[1]); // 4
}
public static void change(int[] arr) {
arr[1] = 4;
}
}
切片
提示
类似java中的List,是一个可变的数组
建:
通过数组创建
slice := arr[1:4]通过make创建
s := make([]int,10)复合声明:
s := []int{1,2,3}
增改:
append(s,4)-- 返回新的切片查:
s[1]删:
- 去头
slice = slice[1:] - 去尾
slice = slice[:len(slice)-1] - 去任意
slice = append(slice[:i], slice[i+1:]...)
- 去头
go中数组有特定的用处,但是却有一些呆板(数组长度固定不可变),所以在 Go 语言的代码里并不是特别常见。相对的切片却是随处可见的,切片是一种建立在数组类型之上的抽象,它构建在数组之上并且提供更强大的能力和便捷
slice 总是指向底层的一个array。slice 是一个指向array 的指针
切片(slice)是对数组一个连续片段的引用,所以切片是一个引用类型。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口
- slice 总是与一个固定长度的array 成对出现,其影响slice 的容量和长度
- append方法可以追加一个元素、多个元素、同类型切片(需要...解构)
- copy方法,超过模板切片大小会自动截断
长度与容量
- 长度是切片的元素数量
- 容量是底层数组可用的大小
package main
import (
"fmt"
)
func main(){
var slice0 []int // 定义 []
arr := [5]int{1,2,3,4,5}
slice := arr[1:4] // 左闭右开
fmt.Println(slice) // [2 3 4]
slice[1] = 9
fmt.Println(arr) // [1 2 9 4 5]
sliceNew := slice[:1] // 简写 arr[:end], arr[start:] , arr[:]
fmt.Println(sliceNew) // [2]
sliceNew[0] = 0
fmt.Println(arr) // [1 0 9 4 5]
// 容量 和 长度不同
fmt.Println(cap(slice)) // 4
fmt.Println(len(slice)) // 3
fmt.Println(cap(sliceNew)) // 4
fmt.Println(len(sliceNew)) // 1
// make底层创建一个数组,对外不可见,所以不可以直接操作这个数组,要通过slice去间接的访问各个元素,不可以直接对数组进行维护/操作
sliceFromMake := make([]int,4,10) // 类型 长度 容量
fmt.Println(sliceFromMake) // [0 0 0 0]
sliceFromMake2 := []int{1,2,3,4}
fmt.Println(sliceFromMake2) // [1 2 3 4]
// 追加后会重新维护一个数组
sliceFromMake2 = append(sliceFromMake2,sliceFromMake...) // 追加切片,注意...
sliceFromMake2 = append(sliceFromMake2,88,50) // 追加元素
fmt.Println(sliceFromMake2) // [1 2 3 4 0 0 0 0 88 50]
// 拷贝
var a = [...]int{0, 1, 2, 3, 4, 5, 6, 7}
var s = make([]int , 6)
n1 := copy(s, a[0:]) // n1 == 6, s == []int{0, 1, 2, 3, 4, 5}
n2 := copy(s, s[2:]) // n2 == 4, s == []int{2, 3, 4, 5, 4, 5}
//拷贝 -- 覆盖
var a []int = []int{1,4,7,3,6,9} // 定义切片
var b []int = make([]int,10) // 再定义一个切片
copy(b,a) // 将a中对应数组中元素内容复制到b中对应下标的中
fmt.Println(b) // [1 4 7 3 6 9 0 0 0 0]
}
映射
提示
类似java中的Map
注意:不能用slice、map、function作为key,因为这几个不能用==做判断
建:
- 通过make创建:
monthdays := make(map[string]int) - 复合声明
m := map[int]string{ 1:"li", 2:"wang", }- 通过make创建:
增改:
monthdays["Feb"] = 29查:
value, present = monthdays["Jan"]删:
delete(monthdays, "Mar")
package main
import (
"fmt"
)
func main(){
// map1 := map[string]string // 仅声明
map1 := make(map[string]string,10)
map2 := make(map[int]string)
map3 := map[int]string{
111:"liyuanhao",
222:"liusisi",
}
map1["name"] = "liyuanhao"
map1["age"] = "18" // 增
map1["age"] = "27" // 改
fmt.Println(map1)
fmt.Println(map2)
fmt.Println(map3)
delete(map3,111) // 删
fmt.Println(map3)
v,b := map3[111] // 查
fmt.Println(v,b) // false
a:=map[int]map[string]string{
1:map[string]string{
"001":"li",
"002":"ni",
},
2:map[string]string{
"001":"ll",
"002":"mm",
},
}
fmt.Println(a)
}
函数
参数、返回值
提示
形参:参数名在前,类型在后
- 支持可变形参(args...int)
多返回值
一个返回值省略括号
多个返回值加括号
可以给返回值起名,和参数用起来一样,在函数开始时,它们会用其类型的零值初始化,结果直接 return
func test(a int,b int)(sum int,sub int){ sum = a+b // 不用声明,直接用 sub = a-b return // 只写return }
没有重载
函数也是一种数据类型
func 函数名 (形参列表)(返回值类型){ // 返回值类型就一个的话,那么()是可以省略不写的 }
package main
import "fmt"
type ex func(*int,*int) // 别名,但是编译时不识别为相同类型,需要强转
func main(){
num1:=10
num2:=20
exchange(&num1,&num2)
fmt.Println(num1,num2)
func1 := exchange
fmt.Printf("exchange函数的类型为:%T\n",func1) // func(*int, *int)
fmt.Printf("exchange函数的类型为:%T\n",exchange) // func(*int, *int)
func1(&num1,&num2)
fmt.Println(num1,num2)
callbake(func1)
callbake2(exchange)
}
func callbake(exchange func(*int,*int)){
fmt.Println("####### callbake")
}
func callbake2(exchange ex){
fmt.Println("xxxxxxxxxxxxxx")
}
func exchange(num1 *int,num2 *int){
t:=*num1
*num1 = *num2
*num2 = t
fmt.Println("####### exchage")
}
func sumAndSub1(m int,n int)(int,int){ // 多返回值
sum := m + n
sub := m - n
return sum,sub
}
func sumAndSub(m int,n int)(sum int,sub int){ // 返回值命名
sum = m + n
sub = m - n
return
}
回调函数
由于函数也是值,所以可以很容易的传递到其他函数里,然后可以作为回调。首先定义一个函数,对整数做一些“事情”:
func printit(x int){
fmt.Println(x)
}
func callback(y int,f func(int)){
f(y)
}
// 使用
callback(1) // 1
init函数
init函数函数比main函数优先执行,全局变量赋值时调用的函数比init先执行,引入的包中的init方法比当前包先执行
package main
import (
"fmt"
"other" // 1、被引用的包中的 全局变量初始化引用的方法 和 init方法先执行
)
var num int= test() // 2、当前包中,全局变量初始化引用的方法再执行
func main(){ // 4、main方法执行
fmt.Println("main 执行了")
}
func test() int { //
fmt.Println("test 执行了")
return 20
}
func init(){ // 3、当前包的init方法执行
fmt.Println("init 执行了")
}
匿名函数
package main
import "fmt"
// 匿名函数赋给全局变量
var fun = func(m int,n int)(max int){
if m > n {
max = m
} else {
max = n
}
return
}
func main(){
// 匿名函数
res := func (m int,n int) (sum int){
sum = m + n
return
}(1,2)
fmt.Println(res)
// 匿名函数赋给变量
f := func (m int,n int) (sub int){
sub = m - n
return
}
fmt.Println(f(5,3))
fmt.Println(fun(8,99))
}
闭包
闭包对内存消耗比较大
package main
import "fmt"
// 全局变量
var n int
func main(){
f:=getSum()
fmt.Println(f(1))
fmt.Println(f(2))
fmt.Println("-------------")
fmt.Println(getSum2(1))
fmt.Println(getSum2(2))
}
// 闭包返回值为一个函数
func getSum() (func (int) int){
sum := 0 // 对于闭包来说,sum类似全局变量
return func (num int) int{
sum = sum + num
return sum
}
}
// 共享变量
func getSum2(m int) int{
n = m + n
return n
}
defer关键字
提示
释放资源原理:遇到defer关键字会压入栈中不执行,遇到return时,从栈中弹出,并且不会改变原值,常用于资源释放
类似java中的 finally
package main
import "fmt"
func main(){
fmt.Println(add(1,2)) // 第四步
}
func add(m int,n int) int{
defer fmt.Println(m) // 第三步
defer fmt.Println(n) // 第二步
sum := m + n
fmt.Println(sum) // 第一步
return sum
}
字符串相关
len(str)
r:=[]rune(str) //切片
for i:=0; i < len(str) ;i++{
fmt.Println("%c \n",r[i])
}
n, err := strconv.Atoi("66") // 转整数 性能比ParseInt略差
str = strconv.Itoa(6887) // 转字符串
strings.Contains("javaandgolang", "go") // 是否包含
strings.Count("javaandgolang","a") // 包含个数
strings.EqualFold("go" , "Go") // 不区分大小写比较 ,区分直接用==
strings.lndex("javaandgolang" , "a") // 第一次出现的位置
strings.Replace("goandjavagogo", "go", "golang", -1) // 替换,-1表示全部替换
strings.Split("go-python-java", "-") // 切割
strings.ToLower("Go") // 转小写
strings.ToUpper"go") // 转大写
strings.TrimSpace(" go and java ") // 去空格
strings.Trim("~golang~ ", " ~") // 去指定
strings.TrimLeft("~golang~", "~") // 去左
strings.TrimRight("~golang~", "~") // 去右
strings.HasPrefix("http://java.sun.com/fmt", "http") // 是否是指定开头
strings.HasSuffix("demo.png", ".png") // 是否是指定结尾
日期时间
package main
import (
"fmt"
"time"
)
func main(){
//时间和日期的函数,需要到入time包,所以你获取当前时间,就要调用函数Now函数:
now := time.Now()
//Now()返回值是一个结构体,类型是:time.Time
fmt.Printf("%v ~~~ 对应的类型为:%T\n",now,now)
//2021-02-08 17:47:21.7600788 +0800 CST m=+0.005983901 ~~~ 对应的类型为:time.Time
//调用结构体中的方法:
fmt.Printf("年:%v \n",now.Year())
fmt.Printf("月:%v \n",now.Month())//月:February
fmt.Printf("月:%v \n",int(now.Month()))//月:2
fmt.Printf("日:%v \n",now.Day())
fmt.Printf("时:%v \n",now.Hour())
fmt.Printf("分:%v \n",now.Minute())
fmt.Printf("秒:%v \n",now.Second())
//这个参数字符串的各个数字必须是固定的,必须这样写
datestr2 := now.Format("2006/01/02 15/04/05")
fmt.Println(datestr2)
//选择任意的组合都是可以的,根据需求自己选择就可以(自己任意组合)。
datestr3 := now.Format("2006 15:04")
fmt.Println(datestr3)
// 字符串转time
timeStr,_ := time.Parse("2006-01-02","2020-08-09")
}
内置函数
--- builtin包下
new // 主要用来分配值类型(int系列, float系列, bool, string、数组和结构体struct)
make // 主要用来分配引用类型(指针、slice切片、map、管道chan、interface 等)
close // 用于channel 通讯。使用它来关闭channel
len // 返回字符串、slice 和数组的长度
cap // 容量
delete // 用于在map 中删除实例
copy // 用于复制slice
append // 用于追加slice
// 异常处理
panic
recover
// 底层打印函数,可以在不引入fmt 包的情况下使用。它们主要用于调试
print
println
// 用于处理复数
complex
real
imag
标准输入
- 标准输入
- fmt.Scanln
- fmt.Scanf
package main
import "fmt"
func main(){
var age int
fmt.Println("请输入age:")
fmt.Scanln(&age)
fmt.Println("age =",age)
var name string
var score float32
var isVIP bool
fmt.Printf("请依次输入,name,age,score,isVIP(空格隔开):")
fmt.Scanf("%s %d %f %t",&name,&age,&score,&isVIP)
fmt.Printf("name = %s, age = %d, score = %f, isVIP = %t",name,age,score,isVIP)
}
恐慌、恢复
提示
类似java的异常
Panic
- 是一个内置函数
- 类似java的throw
- 可以中断原有流程控制,进入 panic的过程 ---- defer遇到return和异常开始从栈中弹出,异常和return后的方法不再执行
Recover
- 也是一个内置函数
- 可以让 panic 中的 goroutine 恢复过来
- 类似java的try-catch
- 只能在defer中使用
- 调用recover后,返回值为nil就是没有panic
errors.New("异常信息")这种只是一个类型为error的值,并不是抛异常,当成一个返回值func main(){ err := test() if err != nil { fmt.Println("出现异常",err) } fmt.Println("继续执行") } func test() (err error){ // 返回值为 error类型 m := 10 n := 0 if n == 0 { return errors.New("除数不能为0") // 把错误信息返回 } else { result := m / n fmt.Println("除法执行了",result) return nil } }
恐慌捕获
package main
import "fmt"
func main(){
defer func(){
err := recover()
if err != nil {
fmt.Println("异常被捕获",err)
}
}()
test()
fmt.Println("继续执行") // 未执行
}
func test(){
// defer func(){
// err := recover()
// if err != nil {
// fmt.Println("异常被捕获",err)
// }
// }()
// result := 10 / 0 // 编译报错
m := 10
n := 0
result := m / n
fmt.Println("除法执行了",result) // 未执行
}
自定义异常 ---- 抛出panic
package main
import (
"fmt"
"errors"
)
func main(){
defer func(){
err := recover()
if err != nil {
fmt.Println("异常被捕获",err)
}
}()
err := test()
if err != nil {
fmt.Println("出现异常",err)
panic(err) // 遇到一个错误,抛出异常
}
fmt.Println("继续执行")
}
包
标识符
包名
- 尽量保持package的名字和目录保持一致,多个文件可以不一致,和标准库不要冲突
- 多个文件使用相同的包名
- 包名是小写的一个单词;不应当有下划线或混合大小写。保持简洁,方便使用,不要过早考虑命名冲突。
如果是编译成可执行程序,main所在的包声明为main包,如果是写库,那么请随意
包名是从$GOPATH/src/后开始计算,使用/分割,与路径一致,GOPATH设置为环境变量
导包用路径
可以给包起别名,但是原来的包名就不能使用了
// 如:在 $GOPATH/src/aaa/bb/c/dbutils 目录下声明一个包
package dddddd
// 引用
import(
"fmt"
"aaa/bb/c/dbutils"
test "aaa/bb/c/redisUtils"
)
func main(){
dddddd.Add()
test.Set()
}
包文档
每个包都应该有包注释,在package 前的一个注释块。
对于多文件包,包注释只需要出现在一个文件前,任意一个文件都可以。
包注释应当对包进行介绍,并提供相关于包的整体信息。这会出现在go doc 生成的关于包的页面上,并且相关的细节会一并显示。
来自官方regexp 包的例子:
/*
The regexp package implements a simple library for
regular expressions.
The syntax of the regular expressions accepted is:
regexp:
concatenation '|' concatenation
*/
package regexp
每个定义的公共函数应当有一小段文字描述该函数的行为。
来自于fmt 包的例子:
// Printf formats according to a format specifier and writes to standard
// output. It returns the number of bytes written and any write error
// encountered.
func Printf(format string, a ...interface) (n int, err error)
测试包
包名 testing
测试文件,以*_test.go命名
待测代码 --- 不需要main函数,随便一个包都可以
测试函数 --- 名字以Test 开头,包含*testing.T形参
// Fail 标记测试函数失败,但仍然继续执行。
func (t *T) Fail()
// FailNow 标记测试函数失败,并且中断其执行。当前文件中的其余的测试将被跳过,然后执行下一个文件中的测试。
func (t *T) FailNow()
// Log 用默认格式对其参数进行格式化,与Print() 类似,并且记录文本到错误日志。
func (t *T) Log(args ... interface{})
// Fatal 等价于Log() 后跟随FailNow()。
func (t *T) Fatal(args ... interface{})
package test
import(
_"fmt"
)
func num(n int) int{
return n
}
测试代码
package test
import(
_"fmt"
"testing"
)
func TestNum(t *testing.T) {
res := num(1)
if res != 1{
t.Fatalf("Num执行错误 \n")
}
t.Logf("Num执行正确 \n")
}
启动测试
## -v 参数可以打印详细的日志
## -cover 覆盖率
## -run regexp 只运行 regexp 匹配的函数,例如 -run=Array 那么就执行包含有 Array 开头的函数
go test -v -cover
常用包
- fmt:实现了格式化的I/O 函数,这与C 的printf 和scanf 类似。格式化短语派生于C
- io:这个包提供了原始的I/O 操作界面。它主要的任务是对os 包这样的原始的I/O 进行封装,增加一些其他相关,使其具有抽象功能用在公共的接口上。
- bufio:这个包实现了缓冲的I/O。它封装于io.Reader 和io.Writer 对象,创建了另一个对象(Reader 和Writer)在提供缓冲的同时实现了一些文本I/O 的功能。
- sort:提供了对数组和用户定义集合的原始的排序功能。
- strconv:提供了将字符串转换成基本数据类型,或者从基本数据类型转换为字符串的功能。
- os:提供了与平台无关的操作系统功能接口。其设计是Unix 形式的。
- sync:提供了基本的同步原语,例如互斥锁。
- flag:实现了命令行解析。
- encoding/json:实现了编码与解码RFC 4627 [2] 定义的JSON 对象。
- html/template:数据驱动的模板,用于生成文本输出,例如HTML。
- 将模板关联到某个数据结构上进行解析。模板内容指向数据结构的元素(通常结 构的字段或者map 的键)控制解析并且决定某个值会被显示。模板扫描结构以 便解析,而“游标” @ 决定了当前位置在结构中的值。
- net/http:实现了HTTP 请求、响应和URL 的解析,并且提供了可扩展的HTTP 服务和基本的HTTP 客户端。
- unsafe:包含了Go 程序中数据类型上所有不安全的操作。通常无须使用这个。
- reflect:实现了运行时反射,允许程序通过抽象类型操作对象。通常用于处理静态类型interface{} 的值,并且通过Typeof 解析出其动态类型信息,通常会返回一个有接口类型Type 的对象。
- os/exec:执行外部命令。
进阶
指针
提示
Go 有指针。然而却没有指针运算,因此它们更象是引用而不是你所知道的来自于C的指针。
在Go 中调用函数的时候,是值传递的。所以想改变基本类型的值,就必须传指针,比如字符串和结构体
| go | c | |
|---|---|---|
| 声明 | var p *int | int *p |
| 内存分配 | new(int)、make() | malloc |
| 数组名 | 整个数组的首地址 | 首元素地址 |
| 指针运算 | 不支持,需要用unsafe包操作地址 | 支持 |
package main
import "fmt"
func main(){
age := 18
var p1 *int // 初始化为nil
p1 = = &age
p2 := &age
fmt.Println(p1)
fmt.Println(p2)
fmt.Println(&p1,&p2)
age2 := *p1
*p1 = 20 // 改值
fmt.Println(age2)
fmt.Printf("p2指向的值为:%v\n",*p2)
}
没有指针运算,如果这样写:*p++,表示(*p)++,取到地址指向的值,然后再++
内存分配
结论
先总结下,细节可以见下面描述
- int系列、float系列、bool、string、数组:用var
- 结构体:用var(返回对象) 或 new(返回指针)
- 由于用对象还是指针,go内部做了优化,所以用var就可以
- slice、map、channel:用make,只声明用var
- 指针:用var
var
var用于声明变量,不一定分配内存- 它可以用于声明任何类型的变量,包括基本类型(如整数、浮点数、布尔值等)和复合类型(如数组、切片、映射等)。
- 内存分配
- 基本类型(int系列、float系列、bool、string、数组、结构体):声明时就内存分配
- 引用类型(指针、slice、channel、interface、map、函数):声明时为nil
- 声明时不分配内存
- slice、map、channel必须make
new
new(T) 分配了零值填充的T 类型的内存空间,并且返回其地址,一个*T 类型的值
说人话就是:分配了一个T类型的内存,并返回了对应的内存地址,成员变量对应的值为零值
new完了就可以直接使用了,和java一致
new (T)和&T{}等价 -----T{}是复合声明和java不同的是:
var m1 myStruct这种形式声明出来的也可以直接使用func main() { var m1 myStruct m2 := new(myStruct) m1.name = "sss" m2.age = 11 fmt.Println(m1, m2) // {0 sss} &{11 } } type myStruct struct { age int name string }
make
只能创建slice,map 和channel,并且返回一个有初始值(非零)的T 类型,而不是指针
这是因为,这三种数据结构在使用前必须初始化
- 例如,一个slice,是对 指向数组的指针,长度,容量 的描述
例如,
make([]int, 10, 100)- 容量: 底层数组分配的内存是100个整型
- 长度:创建了slice 结构指向数组的前10 个元素
- new([]int) 返回指向新分配的内存的指针,零值填充,底层数组是个nil用来干嘛,所以用make创建
构造函数
零值不能满足需求,必须有一个用来初始化的构造函数,例如os包的
func NewFile(fd int , name string) *File { if fd < 0 { return nil } return &File{ fd: fd, name: name } }
自定义类型
类型别名
提示
使用type重新定义(相当于取别名),Golang认为是新的数据类型,但是相互间可以强转
如果要当成原有类型使用,必须强转
别名不能将本来基本类型的转为引用类型
type myInt int
结构体
提示
java的对象不同
结构体传值
字段:可以创建匿名字段 --- 类型名称就是字段名字,有点像js
type MyStruct struct {
x,y int
A *[]int
F func()
}
type MyStruct struct {
T1 // 字段名字是T1
*T2 // 字段名字是T2
P.T3 // 字段名字是T3
}
方法
- 可以与地址绑定,也可以直接和对象绑定,具体看是否改变对象的字段值
- 自定义类型,都可以有方法,而不仅仅是struct,比如int , float32等都可以有方法,起个别名就行
// 结构体 type Teacher struct{ Name string age int School string } // 结构体绑定方法 ---- 传址 func (t Tc)setTeacherName(){ t.Name = "拉阿拉" } // 结构体绑定方法 ---- 传值 func (t *Tc)setTeacherAge(){ t.age = 999 }别名:和其他类型一样,结构体也可以定义别名,但,新的类型只保留字段,没有方法
type newTeacher Teacher nT:= new(newTeacher) // 这个对象只有Teacher的字段,一个方法也没有强转:需要有完全相同的字段(名字、个数和类型),也就是只有别名可以强转
和C也有很大的区别
调用字段和方法时,底层编译器做了优化,底层会自动帮我们加上 & * ,无论通过对象还是通过指针调用,都可以
但是形参不行,形参是什么就必须传什么
示例:声明与赋值
var teacher Teacher // 声明并初始化字段为零值
fmt.Println(teacher) // { 0 }
t1 := Teacher{ // 复合声明,简单写法,相当于全参构造器
"liyuanhao",
18,
"娃哈哈幼儿园",
}
t2 := Teacher{
Name:"li",
age:18,
School:"lallala",
}
t3 := new(Teacher) // 声明并初始化,返回指针
fmt.Println(t3) // &{ 0 }
(*t3).Name = "liu"
fmt.Println(t3) // &{liu 0 }
fmt.Println(*t3) // {liu 0 }
示例:方法调用
t :=new(Tc)
(*t).Name = "叭叭叭"
(*t).setTeacherName() // t传值,非传址
t.setTeacherAge() // 指针传址
fmt.Println(t)
封装
首字母大写 ---- 相当于public,小写相当于private
- 建议将结构体、字段(属性)的首字母小写(其它包不能使用,类似private,实际开发不小写也可能,因为封装没有那么严格)
给结构体所在包提供一个工厂模式的函数,首字母大写(类似一个构造函数)
// 工厂模式 --- 使私有结构体在其他包也能创建实例 type person struct{ name string age int } func NewPerson(name string,age int) *person{ return &person{name,age} }
- 提供一个首字母大写的Set方法(类似其它语言的public),用于对属性判断并赋值
提供一个首字母大写的Get方法(类似其它语言的public),用于获取属性的值
// set方法需要绑定对象指针 - 需要改变属性的都要绑定对象指针 func (p *Person)SetName(name string){ p.name = name } // get方法可以直接绑定对象,指针也可 func (p Person)GetName() string{ return p.name }
组合
提示
java中多组合少继承,在go中继承就是通过组合的形式实现继承的
type Animal struct{
Age int
Weight float32
}
type Cat struct{
animal Animal // 继承
}
组合后就可以使用父结构体的字段和方法啦
如,给父结构体绑定了方法
func (animal *Animal)Shout(){
fmt.Println("我会叫---")
}
// 使用
cat := new(Cat)
cat.animal.Shout()
匿名:能不能像java那样直接使用呢,用匿名字段组合就可以啦
type Cat struct{
Animal // 匿名继承
}
// 使用
cat := new(Cat)
cat.Shout()
package main
import (
"fmt"
)
func main(){
cat := Cat{
Animal{Age:2,
Weight:10.2,
}}
cat2 := new(Cat)
// cat2.Age = 3 // 有名的结构体用名调
cat2.animal.Weight = 999.9
fmt.Println(cat) // {{2 10.2}}
fmt.Println(cat2) // &{{3 999.9}}
cat.Shout() // 喵喵喵---
}
type Animal struct{
Age int
Weight float32
}
func (cat *Cat)Shout(){
fmt.Println("喵喵喵---")
}
父体可以是匿名结构体,匿名字段也可以是基本数据类型
type C struct{
A // 匿名结构体,父类
int // 匿名字段,基本数据类型
}
func main(){
c := C{A{10,"ss"},333}
}
在有匿名结构体时,同名字段、方法,就近原则,先找本体字段,再找父体字段
有名的结构体必须用名调
支持多继承,建议大家尽量不使用多重继承
package main
import (
"fmt"
)
type A struct{
a int
b string
}
type B struct{
c int
d string
}
type C struct{
A
B
}
func main(){
//构建C结构体实例:
c := C{A{10,"aaa"},B{20,"ccc"}}
fmt.Println(c)
}
package main
import (
"fmt"
)
type A struct{
a int
b string
}
type B struct{
c int
d string
a int
}
type C struct{
A
B
}
func main(){
//构建C结构体实例:
c := C{A{10,"aaa"},B{20,"ccc",50}}
// 如嵌入的匿名结构体有相同的字段名或者方法名,则在访问时,需要通过匿名结构体类型名来区分
fmt.Println(c.b)
fmt.Println(c.d)
fmt.Println(c.A.a)
fmt.Println(c.B.a)
}
切片排序
切片排序都是通过sort包中Sort方法实现的
func Sort(data Interface) {
n := data.Len()
if n <= 1 { return }
limit := bits.Len(uint(n))
pdqsort(data, 0, n, limit)
}
Interface是一个接口
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
参考下IntSlice
type IntSlice []int
func (x IntSlice) Len() int { return len(x) }
func (x IntSlice) Less(i, j int) bool { return x[i] < x[j] }
func (x IntSlice) Swap(i, j int) { x[i], x[j] = x[j], x[i] }
自定义结构体切片
type Person struct {
name string
age int
}
type PersonSort []Person
仿写实现
type PersonSort []Person
func (p PersonSort) Len() int { return len(p) }
func (p PersonSort) Less(i, j int) bool { return p[i].age < p[j].age }
func (p PersonSort) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
测试
func main() {
p := PersonSort{}
for i := 0; i < 100; i++ {
person := Person{
age: rand.Intn(80) + 1,
name: fmt.Sprintf("测试 %d 大大大", rand.Intn(100))}
p = append(p, person)
}
sort.Sort(p)
for _, person := range p {
fmt.Println(person)
}
}
接口
提示
struct是定义字段和方法的集合
type S struct{ i int } func (p *S) Get() int { return p.i } func (p *S) Put(v int) { p.i = v }上述定义了一个结构体S,包含了一个字段和两个方法
而interface是定义方法的集合
type I interface{ Get() int Put(int) }上述定义了一个接口I,包含了两个方法
观察发现,结构体的方法是通过绑定做到的,而接口的方法直接定义到了代码块内
对应接口I,S就是一个实现,因为S绑定了I定义的所有方法 --- 这个java差距很大,不知不觉就实现了
使用起来和java还是类似的 ---- 多态
func f(i I){ n := i.Get() i.Put(1) } var s S f(&s)注意
- 通过结构体绑定实现方法,还是指针绑定方法,是不一样的,一般我们统一通过指针绑定,这就意味着,结构体没有实现,实现接口的是结构体的指针,使用时也必须传指针
- 当然,也可以一些绑定到结构体,一些绑定到指针上,只要加起来实现了所有方法,就是接口的一个实现,但是官方不推荐我们这样做,非想用的话,使用时也必须传指针
- 只要全部方法绑定到结构体实例的时候,使用时使用结构体的实例,而不能再用指针了
多态性
提示
相对Java的相同点
实现接口要实现所有的方法才是实现 --- 同Java
接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量 ---- 和java相同
多实现:一个自定义类型可以实现多个接口 --- 和java相同
一个接口可以继承多个别的接口,这时如果要实现接口,也必须将继承的接口的方法也全部实现 --- 和java相同
相对Java的不同点
接口中不能包含任何变量
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型,基本数据类型起别名就行
不需要显式的实现接口。Golang中没有implement关键字,Golang中实现接口是基于方法的,不是基于接口的
例如:
- A接口 a,b方法
- B接口 a,b方法
- C结构体 实现了 a,b方法 ,那么C实现了A接口,也可以说实现了B接口
interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
不能将一个指针指向接口
空接口没有任何方法,所以可以理解为所有类型都实现了空接口,也可以理解为我们可以把任何一个变量赋给空接口
断言、switch-type
类似java的instanceof --- 变量名.(类型)
func f(i I) {
s := i.(*S)
fmt.Println(s.Get())
}
switch-type也类似java的instanceof
switch i.(type){ //type属于go中的一个关键字,固定写法
case S:
case *S:
default:
}
好名字
单方法的接口可以用er后缀,表明职责,如Reader、Writer、Formatter等
众所周知的方法就不用瞎起,如java中的toString,在go中是String,不能定义成ToString
如果一个类型实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出,类似java的toString
// String方法 func (t *Tc) String() string{ return fmt.Sprintf("Name = %s , age = %d , School = %s",t.Name,t.age,t.School) }
接口的组合
type I2 interface {
I // 组合I的方法(类比继承)
Pop()
}
type S struct{ i int }
func (p *S) Get() int { return p.i }
func (p *S) Put(v int) { p.i = v }
func (p *S) Pop() { }
func f2(i I2) {
fmt.Println(i.Get())
}
func main() {
var s = S{2}
f2(&s)
}
自省与反射
提示
- 自省(introspection)是指程序在运行时能够检查、分析和获取自身的信息。通过自省,我们可以获取变量的类型、值、函数的签名和调用栈等信息,以便在运行时做出相应的处理。
- Go中,自省通过反射实现的
先看两个方法:
reflect.TypeOf(any) reflect.Type--- 获取类型- 这里的Type是一个接口,实现是
*reflect.rtype
- 这里的Type是一个接口,实现是
reflect.ValueOf(any) reflect.Value---- 获取值- 每个类型都有对应的转换方法,如Int
func testTypeAndValue(){
num := 100
reType := reflect.TypeOf(num)
reValue:=reflect.ValueOf(num)
fmt.Println(reType) // int
fmt.Println(reValue) // 100
fmt.Printf("reType的类型:%T,reValue的类型:%T \n",reType,reValue) // *reflect.rtype reflect.Value
// 所以不能直接对 reValue 进行操作,需要转换
num = int(reValue.Int() + 8) // 转换后是int,而8 是int64,自动提升,需要用int64接收
fmt.Println(num)
}
再看两个方法:
reType.Kind()、reValue.Kind()--- 都是获取类名称的返回值是Kind,本质是一个uint,打印时调用了实现的String方法
type Kind uint const ( Invalid Kind = iota Bool Int Int8 //... ) func (k Kind) String() string { if uint(k) < uint(len(kindNames)) { return kindNames[uint(k)] } return "kind" + strconv.Itoa(int(k)) }
func testKind(){
num := 100
reType := reflect.TypeOf(num)
reValue:=reflect.ValueOf(num)
// 获取类别
k1:=reType.Kind()
fmt.Println(k1) // int
k2:=reValue.Kind()
fmt.Println(k2) // int
}
结构体
func testStruct(){
person:=Person{Name:"li",age:18}
reType:=reflect.TypeOf(person)
reValue:=reflect.ValueOf(person)
fmt.Println(reType) // main.Person
fmt.Println(reValue) // {li 18}
// 传指针 --- 用于改属性值
reValue1:=reflect.ValueOf(&person)
reValue2 := reValue1.Elem() //拿到 reflect.Value
// 类型断言
i2 := reValue.Interface()
//类型断言:
n,flag := i2.(Person)
if flag {//断言成功
fmt.Printf("名字是:%v,年龄是:%v \n",n.Name,n.age)
}
fmt.Println("--- 结构体类别 ---")
k1:=reType.Kind()
fmt.Println(k1) // int
k2:=reValue.Kind()
fmt.Println(k2) // int
}
拿结构体字段
func GetField(){
person:=Person{Name:"li",age:18}
// 属性值的获取
reValue := reflect.ValueOf(person)
fmt.Println(reValue)
// 根据索引(按声明的顺序)
nf:=reValue.NumField()
for i:=0;i<nf;i++{
fmt.Printf("第%d个字段的值为%v \n",i,reValue.Field(i))
}
// 根据属性名
fmt.Printf("%v \n",reValue.FieldByName("Name"))
}
拿结构体方法 --- 只能获取到公共方法,传结构体对象,只能拿到对象绑定的方法。传指针,能拿到所有方法
func testGetMethod(){
person:=Person{Name:"li",age:18}
// reValue := reflect.ValueOf(person)
reValue := reflect.ValueOf(&person)
nm := reValue.NumMethod() // 获取有几个方法,必须是公共方法
fmt.Println(nm)
}
修改 --- 必须传指针 --- 结构体的字段必须是公共字段才能修改
func testChange(){
// 修改基本类型的值,ValueOf 必须传指针
num5 := 100
reValue5:=reflect.ValueOf(&num5)
reValue5.Elem().SetInt(200)
fmt.Println(num5)
// 属性值的修改
person:=Person{Name:"li",age:18}
reValue1 := reflect.ValueOf(&person)
reValue1.Elem().Field(0).SetString("bibibi")
fmt.Println(person)
}
调用结构体方法
func InvokeMethod(){
person:=Person{Name:"li",age:18}
reValue := reflect.ValueOf(&person)
// 按索引获取(索引为通过ASCCLL排序后的顺序)
rel:=reValue.Method(0).Call(nil)
fmt.Printf("%T \n",rel) // 返回值类型是 []reflect.Value
fmt.Println(rel[0].Int()) // 18
// 构建入参
params := []reflect.Value{reflect.ValueOf("uuuu")}
// 按照方法名调用
reValue.MethodByName("SetName").Call(params)
fmt.Println(person) // {uuuu 18}
}
并发
并行是关于性能的
并发时关于程序设计的
协程
是一种用户态的轻量级线程,仅仅比分配栈空间的消耗多一点点,初始栈空间很好,随着需要在堆上分配和释放空间
本质上是单线程时间片轮询
对于单线程下,我们不可避免程序中出现io操作,
但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)进行任务的切换
- 控制单线程下的多个任务能在一个任务遇到io阻塞时就将寄存器上下文和栈保存到某个其他地方,然后切换到另外一个任务去计算
- 在任务切回来的时候,恢复先前保存的寄存器上下文和栈,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,
- 相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而会更多的将cpu的执行权限分配给我们的线程
线程是CPU控制的,而协程是程序自身控制的,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级)
简单示例
package main
import (
"fmt"
"time"
)
func main(){
go test() // 开启一个协程
for i:=0;i<10;i++{
fmt.Println("---------------- ",i)
time.Sleep(time.Second)
}
}
func test(){
for i:=0;i<10;i++{
fmt.Println("###### ",i)
time.Sleep(time.Second)
}
}
runtime包
提供了与 Go 程序执行时的运行时环境相关的函数和变量。它包含了许多与并发、调度、内存管理等相关的功能
- 并发控制
Gosched()用于让出处理器以允许其他 goroutine 运行,类似于 Java 中的Thread.yield()方法Goexit()用于终止当前 goroutine 的执行
- 内存管理
GOMAXPROCS变量用于设置可同时执行的最大 CPU 数量ReadMemStats()用于读取内存统计信息
- 反射支持:
FuncForPC()用于返回与给定程序计数器值对应的函数。 - 锁定机制:
LockOSThread()和UnlockOSThread()用于在当前 OS 线程上锁定和解锁 goroutine。 - 垃圾回收控制:
SetFinalizer()用于设置对象的终结器函数,GC()用于手动触发垃圾回收。
管道
语法
先简单认识下管道的语法
// 声明
var c chan int // 可用声明不同类型的管道
// 内存分配
c = make(chan int) // 可以指定缓冲区大小,不指定就是没有
// 向管道写入
c<-1
// 从管道取出
i := <-c
close(chan1) // 关闭后只能读,不能写
// 只读管道
var cr <-chan int
// 只写管道
var cw chan<- int
提示
死锁情况
- 没有缓冲区,只写不读
- 有缓冲区,但不够大,阻塞在等待释放的时候写入(放满不能再放入),导致死锁
- 在遍历时,如果 channel 已经关闭,则会正常遍历数据,遍历完后,就会退出遍历;如果 channel 没有关闭,则出现死锁
- 方式一:写完后关闭chan
- 方式二:用select-case-default
解决死锁
- sync.WaitGroup计数器,适用于管道需要关闭
- select-case-default多路复用,适用于管道不关闭
我们用管道解决一下主死从随的问题
var c chan int
func main(){
go test() // 开启一个协程
go test() // 再开启一个协程
fmt.Println("-------main结束--------- ")
<-c // 阻塞,直到读到数据
<-c
}
func test(){
for i:=0;i<10;i++{
fmt.Println("###### ",i)
time.Sleep(time.Second)
}
c<-1 // 将1写入管道
}
这样还是很恶心,开两个goroutine就需要读两次,还得统计启动了多少个goroutine?
select-case-default
提示
select关键字可以监听管道上数据的流动
和switch非常相似,但是select中的每个case必须是管道IO操作(要么读,要么写)
按顺序评估每一个管道IO
- 多个case都满足,就任意选择一条执行
- 在没有default的情况下,都匹配不上就会阻塞,直到有一个通信(通道IO)可以执行
- 有default就永远不会阻塞,可以用来防止阻塞,但是这是非常消耗CPU的(一直空转)
for {
select {
case i := <-c: // select会一直等待,直到某个case的通信操作完成时,就会执行case分支对应的语句
println(i)
default: // 防止阻塞
time.Sleep(time.Second)
fmt.Println("无数据")
}
}
使用
select语句能提高代码的可读性。可处理一个或多个channel的发送/接收操作
如果多个
case同时满足,select会随机选择一个对于没有
case的select{}会一直等待,可用于阻塞main函数
select{ case <-ch1: ... case data := <-ch2: ... case ch3<-data: ... default: 默认操作 }
定时器
定时器
time.Timer 类似js中的 setTimeout,
func main(){
timer1 := time.NewTimer(2 * time.Second) // 创建
<-timer1.C // 触发
fmt.Println("定时器 1 已触发")
timer2 := time.NewTimer(1 * time.Second)
time2.Reset(3 * time.Second) // 重置
go func() {
<-timer2.C
fmt.Println("定时器 2 已触发")
}()
stop2 := timer2.Stop() // 取消
if stop2 {
fmt.Println("定时器 2 已取消")
}
}
延时器
time.Tick 类似js中的setInterval
func main(){
ticker := time.NewTicker(time.Second)
i := 0
for {
<-ticker.C
i++
fmt.Println("i=", i)
if i==5 {
ticker.Stop()
break
}
}
}
用select实现超时
func main(){
ch := make(chan int)
quit := make(chan bool)
go func() {
for {
select {
case num := <-ch:
fmt.Println(num)
case <-time.After(5 * time.Second):
// 一些操作
quit <- true
}
}
}()
for i := 0; i < 5; i++ {
ch <- i
}
<-quit
}
异常情况

关闭已经关闭的channel也会引发panic
数据结构
type hchan struct {
gcount uint // 环形队列剩余元素个数
dataqsiz uint // 环形队列长度
buf unsafe.Pointer // 环形队列指针
elemsize uint16 // 每个元素大小
closed uint32 // 标识关闭状态
elemtype *_type // 元素类型
sendx uint // 下一个元素写入时的下标
recvx uint // 下一个元素读取时的下标
recvq waitq // 等待读消息的队列
sendq waitq // 等待写消息的队列
lock mutex // 互斥锁, 保障管道无法并发读写
}
写的时候,会对recvq判空
- 非空,则取出一个读协程,写入数据,唤醒
- 空,则判断buf空间是否足够,足够就在尾部追加
- 不够就把当前的写协程写入sendq,阻塞
读的时候,会对sendq判空
- 非空,判断buf是否为空,为空则取出sendq一个写协程,读数据,唤醒
- buf不为空,直接从buf中读,在sendq取出一个协程放到buf,唤醒
- 空,判断gcount(buf中的数据数)是否大于0,是,从buf中取数据
- 小于0,buf中无数据,则放到recvq阻塞
package main
import (
"fmt"
)
func main(){
chan1 := make(chan int,3)
chan1<-10
chan1<-20
chan1<-30
n1:=<-chan1
fmt.Println(n1)
chan1<-40
close(chan1) // 关闭后只能读,不能写
fmt.Println(<-chan1)
// 遍历
for v:=range chan1{
fmt.Println(v)
}
var chanInt chan int // 可读可写
var chanInt1 chan<- int // 只写
var chanInt2 <-chan int // 只读
chanInt = make(chan int,4)
chanInt1 = make(chan<- int,5)
chanInt2 = make(<-chan int,6)
fmt.Println(chanInt,chanInt1,chanInt2)
}
并发问题
主死从随
解决方案1 --- sync.WaitGroup中的三个方法Add(int)、Done()、Wait()
package main
import (
"fmt"
"time"
"sync"
)
var wg sync.WaitGroup
func main(){
wg.Add(1)
go test() // 开启一个协程
for i:=0;i<10;i++{
fmt.Println("---------------- ",i)
}
wg.Wait()
}
func test(){
for i:=0;i<10;i++{
fmt.Println("###### ",i)
time.Sleep(time.Second)
}
wg.Done()
}
解决方案2 --- 管道
package main
import (
"fmt"
_"time"
)
func main(){
productChan := make(chan int ,100)
exitChan := make(chan bool,4)
fmt.Println("____")
for i:=0;i<3;i++{
go write(productChan,100)
}
go read(productChan,exitChan,300)
for ;<-exitChan;{
continue
}
}
func read(productChan <-chan int,exitChan chan<- bool,count int){
for i:=0;i<count;i++{
fmt.Println("取出",<-productChan)
}
exitChan<-true
}
func write(productChan chan<- int,count int){
for i:=0;i<count;i++{
productChan<-i
// fmt.Println("存入",i)
}
}
阻塞死锁
提示
死锁情况
- 没有缓冲区,只写不读
- 有缓冲区,但不够大,阻塞在等待释放的写入,导致死锁
- 循环判断,读到空,会阻塞死锁,应该用select-case-default
解决死锁
- sync.WaitGroup计数器,适用于管道需要关闭
- select-case-default多路复用,适用于管道不关闭
func test1(){
// 无缓冲,只写不读会死锁
chan1 := make(chan int)
chan1<-10
fmt.Println("########")
}
func test3(){
// 缓存区太小,仍然出现死锁
chan1 := make(chan int,1)
chan1<-10
chan1<-10 // 死锁
fmt.Println("#######")
}
package main
import (
"fmt"
"time"
)
func main(){
chan1:=make(chan int ,10)
for i:=0;i<4;i++{
go write(chan1)
}
// ---- 以下两种循环判断,读到空,都会阻塞死锁 ----
for v := range chan1{
fmt.Println(v)
}
for{
if v,ok := <-chan1;!ok{
break
}else{
fmt.Println(v)
}
}
// ---- 以上两种循环判断,读到空,都会阻塞死锁 ----
}
func write(chan1 chan<- int){
time.Sleep(time.Second)
chan1<-10
}
IO
Go 的I/O 核心是接口io.Reader 和io.Writer
文件
提示
os.OpenFile("d:/Demo.txt",os.O_RDWR | os.O_APPEND | os.O_CREATE,0666)参数一:打开或新建的文件
- 参数二:打开模式
- O_RDWR 读写、O_RDONLY 只读、O_WRONLY 只写
- O_APPEND 追加到尾部、O_CREATE 没有就创建
- 。。。
- 参数二:打开模式
参数三:权限控制 --- (linux/unix系统下才生效,windows下设置无效)
os.File封装了所有的文件操作,File是一个结构体,实现了io.Reader和io.Writer- f.Close()
func main() {
file, err := os.Open("D:\\GoLandProjects\\wechatbot\\.gitignore")
defer func(file *os.File) {
err := file.Close()
if err != nil {
}
}(file)
if err != nil {
fmt.Println("xxxxxxxxxxx ", err)
} else {
fmt.Printf("文件=%v", file)
}
}
读
无缓冲
func main(){
buf := make([]byte,1024)
f,_ := os.Open("D:\\GoLandProjects\\wechatbot\\.gitignore")
defer f.Close()
for{
n,_:=f.Read(buf) // 一次读取1024 字节
if n==0 {break} // 到达末尾
os.Stdout.Write(buf[:n]) // 写入 os.Stdout
}
}
二合一
text, err := os.ReadFile("D:\\GoLandProjects\\wechatbot\\.gitignore")
if err != nil {
fmt.Println("xxxxxxxxxxxxxx ", err)
} else {
// fmt.Printf("%v",text) // 输出阿斯科马
fmt.Printf("%v", string(text))
}
欣赏下源码
func ReadFile(name string) ([]byte, error) {
f, err := Open(name)
if err != nil {
return nil, err
}
defer f.Close()
// 获取文件大小
var size int
if info, err := f.Stat(); err == nil {
size64 := info.Size() // Stat()获取到fs.FileInfo,包含文件大小
if int64(int(size64)) == size64 {
size = int(size64)
}
}
// EOF 作为一个字节
size++ // one byte for final read at EOF
if size < 512 {
size = 512
}
data := make([]byte, 0, size)
for {
if len(data) >= cap(data) {
d := append(data[:cap(data)], 0)
data = d[:len(data)]
}
n, err := f.Read(data[len(data):cap(data)])
data = data[:len(data)+n]
if err != nil {
if err == io.EOF {
err = nil
}
return data, err
}
}
}
写
写 --- 输出流
package main
import (
"fmt"
"os"
"io"
"bufio"
"io/ioutil"
)
func main(){
file,err1 := os.Open("./fileTest.go") // 开
newFile,err2 := os.OpenFile("./test.txt",os.O_RDWR | os.O_APPEND | os.O_CREATE,0666)
if err1 != nil || err2 != nil{
fmt.Println("文件打开失败",err1,err2)
return
}
defer file.Close()
defer newFile.Close()
reader := bufio.NewReader(file)
writer := bufio.NewWriter(newFile)
BufferCopy(reader,writer)
simpleCopy()
}
// 利用 bufio包实现 文件复制
func BufferCopy(reader *bufio.Reader,writer *bufio.Writer){
for{
str,err1 := reader.ReadString('\n') // 读一行
if err1 == io.EOF{ // //io.EOF 表示已经读取到文件的结尾
break
}
_,err2 := writer.WriteString(str) // 写到缓冲区
if err2 != nil {
fmt.Println("xxxxxxxxxxxxxxx ",err2)
}
}
writer.Flush() // 刷新缓冲区,真正写入
s :=os.FileMode(0666).String() // 查看权限
fmt.Println(s)
fmt.Println("vvvvvvvvvvvvvvv")
}
// 利用ioutil包实现 文件复制
func simpleCopy(){
if content,err := ioutil.ReadFile("./test.txt") ;err != nil{
fmt.Println("读取有问题!")
return
} else {
err = ioutil.WriteFile("./test2.txt",content,0666)
if err != nil {
fmt.Println("写出失败!")
}
}
}
缓冲
提示
bufio缓存包,两个重要方法
- r := bufio.NewReader(f) --- 返回值是
bufio.Reader,实现了io.Reader接口 - w := bufio.NewWriter(os.Stdout) --- --- 返回值是
bufio.Writer,实现了io.Writer接口
func main(){
buf := make([]byte,1024)
f,_ := os.Open("D:\\GoLandProjects\\wechatbot\\.gitignore")
defer f.Close()
r := bufio.NewReader(f)
w := bufio.NewWriter(os.Stdout)
defer w.Flush()
for{
n,_:=r.Read(buf) // 一次读取1024 字节
if n==0 {break} // 到达末尾
w.Write(buf[:n]) // 写入 os.Stdout
}
}
缓冲包方法很多
- n,_:=r.Read(缓冲区) --- n==0表示文件末尾
- w.Write(buf[:n])
- str := r.ReadString(分隔符) --- io.EOF 表示文件的末尾
- w.WriteString(str)
- w.Flush()
命令行
提示
命令行的参数在程序中通过字符串切片 os.Args 获取
flag 包有着精巧的接口,同样提供了解析标识的方法
初步认识
func main() {
for i, arg := range os.Args {
fmt.Println(i, "---------", arg)
}
}
执行go run main.go 11 22 33
结果
0 --------- C:\Users\EXT~1.LIY\AppData\Local\Temp\go-build3558467436\b001\exe\main.exe
1 --------- 11
2 --------- 22
3 --------- 33
解析
上述方式比较原生,解析复杂的比较麻烦,如main.go -f ./aaa.txt -p 200 -u root 这样的形式命令行
go 设计者给我们提供了 flag包,可以方便的解析命令行参数,而且参数顺序可以随意
func main() {
var user string
var port int
flag.StringVar(&user, "u", "root", "用户,默认root'")
flag.IntVar(&port, "p", 3306, "端口号,默认3306")
flag.Parse()
fmt.Println(user)
fmt.Println(port)
}
执行
go run .\main.go -p 9999
root
9999
执行命令??
提示
os/exec 包有函数可以执行外部命令,这也是在Go 中主要的执行命令的方法。通过定义一个有着数个方法的*exec.Cmd 结构来使用。
如,执行 ls -l
func TestName(t *testing.T) {
cmd := exec.Command("/bin/ls", "-l")
buf, _ := cmd.Output()
fmt.Println(string(buf))
}
Json序列化
提示
引入"encoding/json"包,两个核心方法
- json.Marshal()
- json.UnMarshal()
序列化时,一般传结构体即可,但本质上应该传指针,做了优化
反序列化时,必须传指针
结构体的属性必须大写,可以指定tag返回给前端
序列化返回的是字节数组,需要强转为string,反序列化同理,需要转回[]byte才能反序列化
序列化
type Person struct{
Name string `json:"name"` // tag 修改序列化后的字段名称,方便前端使用
Age int
}
func testStrut(){
person:=Person{Name:"牛魔王",Age:11}
data,err:=json.Marshal(person)
if err!=nil{
fmt.Println(err)
}else{
fmt.Println(string(data))
}
}
func testMap(){
map1 := make(map[string]interface{},3)
map1["name"] = "孙悟空"
map1["age"] = 11
map2 := map[string]interface{}{
"name":"玉皇大帝",
"age":11,
}
data1,err:=json.Marshal(map1)
data2,_:=json.Marshal(map2)
if err!=nil{
fmt.Println(err)
}else{
fmt.Println(string(data1))
}
fmt.Println(string(data2))
}
func testSlice(){
map1 := make(map[string]interface{},3)
map1["name"] = []string{"孙悟空","美猴王"}
map1["age"] = 11
map2 := map[string]interface{}{
"name":"玉皇大帝",
"age":11,
}
var s []map[string]interface{}
s = append(s,map2,map1)
data,_:=json.Marshal(s)
fmt.Println(string(data))
}
反序列化
type Person struct{
Name string `json:"name"` // tag 修改序列化后的字段名称,方便前端使用
Age int
}
func testStrut(){
person2:=Person{}
str := "{\"name\":\"牛魔王\",\"Age\":11}"
json.Unmarshal([]byte(str),&person2) // 必须传地址
fmt.Println("##### ",person2)
}
func testMap(){
map3 := map[string]interface{}{}
str:="{\"age\":11,\"name\":\"孙悟空\"}"
json.Unmarshal([]byte(str),&map3)
fmt.Println("##### ",map3)
}
func testSlice(){
str := "[{\"age\":11,\"name\":\"玉皇大帝\"},{\"age\":11,\"name\":[\"孙悟空\",\"美猴王\"]}]"
var s2 []map[string]interface{}
json.Unmarshal([]byte(str),&s2)
fmt.Println("##### ",s2)
}
网络
TCP编程
客户端
package main
import(
"fmt"
"net"
"bufio"
"os"
)
func main(){
// 客户端启动
fmt.Println("客户端启动")
conn,err := net.Dial("tcp","127.0.0.1:8888")
if err!=nil{
fmt.Println("连接失败")
return
}
for{
//通过客户端发送单行数据
reader := bufio.NewReader(os.Stdin)//os.Stdin代表终端标准输入
//从终端读取一行用户输入的信息:
str,err := reader.ReadString('\n')
if err != nil {
fmt.Println("终端输入失败,err:",err)
}
//将str数据发送给服务器:
n,err := conn.Write([]byte(str))
if err != nil{
fmt.Println("连接失败,err:",err)
}
fmt.Printf("终端数据通过客户端发送成功,一共发送了%d字节的数据\n",n)
}
}
服务端
package main
import(
"fmt"
"net"
)
func main(){
// 服务端启动
fmt.Println("服务端启动")
listen,err := net.Listen("tcp","127.0.0.1:8888")
if err!=nil{
fmt.Println("监听失败",err)
return
}
for{
conn,err2:=listen.Accept()
if err2!=nil{
fmt.Println("等待接收失败",err2)
}else{
fmt.Println("收到了",conn,conn.RemoteAddr().String())
go read(conn)
}
}
}
func read(conn net.Conn){
defer conn.Close()
for{
text := make([]byte ,1024)
n,err:=conn.Read(text)
if err!=nil{
return
}
fmt.Println(string(text[0:n]))
}
}
HTTP编程
服务端
func main() {
// 注册处理函数,只要用户连接,就可以自动调用处理函数
http.HandleFunc("/", HandConn)
http.ListenAndServe("localhost:8080", nil)
}
func HandConn(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello"))
}
客户端
func main() {
url := "http://www.bilibili.com/";
resp, err := http.Get(url);
if err != nil {
fmt.Println("err =", err);
return;
}
//
defer resp.Body.Close();
//
fmt.Println("Status=", resp.Status);
fmt.Println("Status=", resp.StatusCode);
fmt.Println("Header=", resp.Header);
fmt.Println("Body=", resp.Body);
//
buf := make([]byte, 1024*4);
var tmp string;
//
for {
n, readErr := resp.Body.Read(buf);
if n == 0 {
fmt.Println("readErr =", readErr);
break;
}
tmp += string(buf[:n]);
}
//
fmt.Println("tmp", tmp);
}
