Golang基础知识点整理
两种执行流程的方式区别
- 如果我们先编译生成了可执行文件,那么我们可以将该可执行文件拷贝到没有go开发环境的机器上,仍然可以运行
- 如果我们是直接
go run
go 源代码,那么如果要在另外一个机器上这么运行,也需要go开发环境,否则无法执行 - 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件变大了很多
程序开发注意事项
- go源文件以“go”为扩展名
- go应用程序的执行入口时main()函数
- go语言严格区分大小写
- go方法由一条条语句构成,每个语句后不需要分号(go语言会在每行后自动加分号),这也体现出golang的简洁性
- go编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否则报错
- go语言定义的变量或者import的包如果没有使用到,代码不能编译通过
- 大括号都是成对出现的,缺一不可
程序中"\r"回车的效果
\r 回车,从当前行的最前面开始输出,覆盖掉前面相同长度的内容。
例如:
fmt.Println("hello word\rhhhhh")
输出:
hhhhh word
程序的注释
行注释://
块注释:/* 注释内容 */
变量使用注意事项
-
变量表示内存中的一个存 储区域
-
该区域有自己的名称(变量名)和类型(数据类型)
-
Golang变量使用的三种方式
- 第一种:指定变量类型,声明后若不赋值,使用默认值
- 第二种:根据值自行判定变量类型(类型推导)
- 第三种:省略var,注意:=左侧的变量不应该是已经声明过的,否则会导致编译错误
// “:=”只能在声明“局部变量”的时候使用,而“var”没有这个限制。 name := "asdf" fmt.Println(name) a := 100 b := 12.34 c := 'c' d := "" e := false fmt.Printf("a=%T,b=%T,c=%T,d=%T,e=%T", a, b, c, d, e) // a=int,b=float64,c=int32,d=string,e=bool
-
多变量声明
在编程中,有时我们需要一次性声明多个变量,Golang也提供这样的语法
程序中“+“号的使用
- 当左右两边都是数值型时,则做加法运算
- 当左右两边都是字符串,则做字符串拼接
变量的数据类型
- 基本数据类型
- 数值型
- 整数类型(int,int16,int32,int64,uint,uint8,uint32,uint64,byte)
- 浮点类型(float32,float64)
- float64位的精度,比float32位的精度要高
- 浮点类型有固定的范围和字段长度,不受具体os(操作系统)的影响
- 浮点类型默认声明为float64类型
- 字符型(没有专门的字符型,使用byte来保存单个字母字符)
- 字符常量是用单引号括起来的单个字符
- go中允许使用转义字符''来将其后的字符转变为特殊字符型常量
- go语言的字符使用utf-8编码
- 在go中,字符的本质是一个整数,直接输出时,是该字符对应的utf-8编码的码值
- 可以直接给某个变量赋一个数字,然后按格式化输出时%c,会输出该数字对应的unicode字符
- 字符类型是可以进行运算的,相当于一个整数,因为它都对应有unicode码
- go语言的编码统一成了utf-8编码
- 布尔型(bool)
- bool类型占用存储空间是1个字节
- bool类型只能取true或者false
- 字符串(string)
- 字符串就是一串固定长度的字符连接起来的字符序列。go的字符串是由单个字节连接起来的。也就是说对于传统的字符串是由字符组成的,而go的字符串不同,它是由字节组成的
- go语言的字符串的字节使用utf-8编码标识unicode文本
- 字符串一旦赋值了,字符串就不能修改了:在go中字符串是不可变的
- 字符串的两种表现形式
- 双引号,会识别转义字符
- 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果
- 字符串的拼接方式
+
、+=
,当使用+
号进行字符串拼接时候,多行处理中,上一行要以+
结尾
- 数值型
- 派生/复杂数据类型
- 指针(Pointer)
- 数组
- 结构体(struct)
- 管道(Channel)
- 函数(也是一种类型)
- 切片(slice)
- 接口(interface)
- map
整型的类型
类型 | 有无符号 | 占用存储空间 | 表数范围 | 备注 |
---|---|---|---|---|
int8 | 有 | 1字节 | -128 ~ 127 | |
int16 | 有 | 2字节 | -$2^{15}$ ~ $2^{15}$ - 1 | |
int32 | 有 | 4字节 | -$2^{31}$ ~ $2^{31}$ - 1 | |
int64 | 有 | 8字节 | -$2^{63}$ ~ $2^{63}$ - 1 | |
uint8 | 无 | 1字节 | 0 ~ 255 | |
uint16 | 无 | 2字节 | 0 ~ $2^{16}$ - 1 | |
uint32 | 无 | 4字节 | 0 ~ $2^{32}$ - 1 | |
uint64 | 无 | 8字节 | 0 ~ $2^{64}$ - 1 | |
int | 有 | 32位系统4个字节 64位系统8个字节 |
-$2^{31}$ ~ $2^{31}$ - 1 -$2^{63}$ ~ $2^{63}$ - 1 |
|
uint | 无 | 32位系统4个字节 64位系统8个字节 |
0 ~ $2^{32}$ - 1 0 ~ $2^{64}$ - 1 |
|
rune | 有 | 与int32一样 | -$2^{31}$ ~ $2^{31}$ - 1 | 等价int32表示一个unicode码 |
byte | 无 | 与uint8一样 | 0 ~ 255 | 当要存储字符时选byte |
整型的类型
类型 | 占用存储空间 | 表数范围 |
---|---|---|
单精度float32 | 4字节 | -3.403E38 ~ 3.403E38 |
双精度float64 | 8字节 | -1.798E308 ~ 1.798E308 |
变量的数据类型默认值
package main
import "fmt"
func main() {
var a int
var b float32
var c float64
var d byte
var e bool
var f string
fmt.Printf("a=%d,b=%f,c=%f,d=%v,e=%t,f=%s", a, b, c, d, e, f)
}
// a=0,b=0.000000,c=0.000000,d=0,e=false,f=
基本数据类型的相互转换
go 在不同类型的变量之间赋值时需要显式转换。也就是说go中数据类型不能自动转换
在转换中,比如将 int64 转成 int8,编译时不会报错,只是转换的结果是按溢出处理,和我们希望的结果不一样
var a int64 = 999999
var b int8 = int8(a)
fmt.Printf("a=%d,b=%d", a, b)
// a=999999,b=63
package main
func main() {
var n1 int32 = 12
var n2 int64
var n3 int8
n2 = n1 + 20 // 编译不通过,因为n1的数据类型为int32,[n1 + 20]的结果类型也为int32,无法将int32类型的值赋给int64类型的变量
n3 = n1 + 20 // 编译不通过,因为n1的数据类型为int32,[n1 + 20]的结果类型也为int32,无法将int32类型的值赋给int8类型的变量
}
package main
func main() {
var n1 int32 = 12
var n2 int8
var n3 int8
n2 = int8(n1) + 127 // 编译通过,但是结果按溢出值处理
n3 = int8(n1) + 128 // 编译不通过,因为128,直接报范围溢出
}
package main
import (
"fmt"
"strconv"
)
func main() {
var a int8 = 100
var b float64 = 12.34
var c byte = 'c'
var d string = ""
var e bool = false
d = fmt.Sprintf("%d,%f,%v,%t", a, b, c, e)
fmt.Printf("%T:%s\n", d, d)
d = strconv.FormatInt(int64(a), 10)
fmt.Printf("%T:%s\n", d, d)
// Itoa是FormatInt(i, 10) 的简写。
d = strconv.Itoa(int(a))
fmt.Printf("%T:%s\n", d, d)
d = strconv.FormatFloat(b, 'f',10, 64)
fmt.Printf("%T:%s\n", d, d)
d = strconv.FormatBool(e)
fmt.Printf("%T:%s\n", d, d)
fmt.Println("-------------------")
var a1 = "100"
var b1 = "123.234"
var c1 = "true"
var c3 = ""
var c4 = "1"
var c5 = "0"
var a2 int64
var b2 float64
var c2 bool
a2, _ = strconv.ParseInt(a1, 10, 64)
fmt.Printf("%T:%d\n", a2, a2)
b2, _ = strconv.ParseFloat(b1, 64)
fmt.Printf("%T:%f\n", b2, b2)
c2, _ = strconv.ParseBool(c1)
fmt.Printf("%T:%t\n", c2, c2)
c2, _ = strconv.ParseBool(c3)
fmt.Printf("%T:%t\n", c2, c2)
c2, _ = strconv.ParseBool(c4)
fmt.Printf("%T:%t\n", c2, c2)
c2, _ = strconv.ParseBool(c5)
fmt.Printf("%T:%t\n", c2, c2)
}
/*
string:100,12.340000,99,false
string:100
string:100
string:12.3400000000
string:false
-------------------
int64:100
float64:123.234000
bool:true
bool:false
bool:true
bool:false
*/
指针
- 基本数据类型,变量存的就是值,也叫值类型
- 获取变量的地址,用&
- 指针的类型,指针变量存的是一个地址,这个地址指向的空间存的才是值
- 获取指针类型所指向的值,使用*
package main
import "fmt"
func main() {
var i int = 10
fmt.Printf("变量i的内存地址为:%v", &i)
fmt.Printf("\n变量i的值为:%v", i)
var j *int = &i
fmt.Printf("\n变量j的内存地址为:%v", &j)
fmt.Printf("\n变量j的值为:%v", j)
fmt.Printf("\n变量j的值指向的值为:%v", *j)
i = 20
fmt.Println("\n---------------------------")
fmt.Printf("\n修改变量i后,变量i的内存地址为:%v", &i)
fmt.Printf("\n修改变量i后,变量i的值为:%v", i)
fmt.Printf("\n修改变量i后,变量j的内存地址为:%v", &j)
fmt.Printf("\n修改变量i后,变量j的值为:%v", j)
fmt.Printf("\n修改变量i后,变量j的值指向的值为:%v", *j)
*j = 30
fmt.Println("\n---------------------------")
fmt.Printf("\n修改变量j后,变量i的内存地址为:%v", &i)
fmt.Printf("\n修改变量j后,变量i的值为:%v", i)
fmt.Printf("\n修改变量j后,变量j的内存地址为:%v", &j)
fmt.Printf("\n修改变量j后,变量j的值为:%v", j)
fmt.Printf("\n修改变量j后,变量j的值指向的值为:%v", *j)
}
/*
变量i的内存地址为:0xc0000b2008
变量i的值为:10
变量j的内存地址为:0xc0000ac020
变量j的值为:0xc0000b2008
变量j的值指向的值为:10
---------------------------
修改变量i后,变量i的内存地址为:0xc0000b2008
修改变量i后,变量i的值为:20
修改变量i后,变量j的内存地址为:0xc0000ac020
修改变量i后,变量j的值为:0xc0000b2008
修改变量i后,变量j的值指向的值为:20
---------------------------
修改变量j后,变量i的内存地址为:0xc0000b2008
修改变量j后,变量i的值为:30
修改变量j后,变量j的内存地址为:0xc0000ac020
修改变量j后,变量j的值为:0xc0000b2008
修改变量j后,变量j的值指向的值为:30
*/
值类型和引用类型
-
值类型
- 基本数据类型int系列,float系列,bool,string,数组和结构体struct
- 变量直接存储值,内存通常在栈中分配
-
引用类型
- 指针,slice切片,map,管道chan,interface等都是引用类型
- 变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收
标识符
- 概念
- go 对各种变量、方法、函数等命名时使用的字符序列称为标识符
- 凡是自己可以起名字的地方都叫标识符
- 命名规则
- 由26个英文字母大小写,0-9,_组成
- 数字不可以开头
- 严格区分大小写
- 标识符不能包含空格
- 下划线
_
本身在go中是一个特殊的标识符,称为空标识符。可以代表任何其它的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用。 - 不能以系统保留关键字作为标识符,比如 break 、if 等等
- 注意事项
- 包名:保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,和标准库不要冲突
- 变量名、函数名、常量名:采用驼峰法
- 如果变量名、函 数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用(注:可以简单的理解成,首字母大写是公有的,首字母小写的是私有的)
运算符
运算符 | 运算 | 范例 | 结果 |
---|---|---|---|
+ | 正号 | +3 | 3 |
- | 负号 | -4 | -4 |
+ | 加 | 5 + 5 | 10 |
- | 减 | 6 - 4 | 2 |
* | 乘 | 3 * 4 | 12 |
/ | 除 | 5 / 5 | 1 |
% | 取模 | 7 % 5 | 2 |
++ | 自增 | a=2 a++ | a=3 |
-- | 自减 | a=2 a-- | a=1 |
+ | 字符串相加 | "He" + "llo" | "Hello" |
package main
import (
"fmt"
)
func main() {
// 如果运算的数都是整数,那么除后,去掉小数部分,保留整数部分
fmt.Println(10 / 4) // 结果,2
var n1 float32 = 10 / 4 // 结果,2
fmt.Println(n1)
// 如果希望保留小数部分,则需要有浮点数参与运算
var n2 float32 = 10.0 / 4
fmt.Println(10.0 / 3) // 结果,3.3333333333333335
fmt.Println(n2) // 结果,2.5
// 取模公式:a % b = a - a / b * b
fmt.Println(10 % 3)
var i int = 1
i++
fmt.Println(i)
i++
fmt.Println(i)
// 下面的语言使用方式错误,++、--只能独立使用,无法直接使用到表达式中
//var j int = i++
//var k int = i--
// 下面的语言使用方式错误,不允许前++、--
//++i
//--i
}
关系运算符
运算符 | 运算 | 范例 | 结果 |
---|---|---|---|
== | 相等于 | 4==3 | false |
!= | 不等于 | 4!=3 | true |
< | 小于 | 4<3 | false |
> | 大于 | 4>3 | true |
<= | 小于等于 | 4<=3 | false |
>= | 大于等于 | 4>=3 | true |
逻辑运算符
假定 A 值为 true,B 值为 false
运算符 | 描述 | 实例 |
---|---|---|
&& | 逻辑与运算符。如果两边的操作数都是true,则为true,否则false | (A&&B) 为false |
|| | 逻辑或运算符。如果两边的操作数有一个true,则为true,否则为false | (A||B) 为true |
! | 逻辑非运算符。如果条件为true,则逻辑为false,否则true | !(A&&B) 为true |
- &&也叫短路与:如果第一个条件为false,则第二个条件不会判断,最终结果为false
- ||也叫短路或:如果第一个条件为true,则第二个条件不会判断,最终结果为true
赋值运算符
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,将一个表达式的值赋给一个左值 | 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 <<= A 等于 C = C << A |
>>= | 右移后赋值 | C >>= A 等于 C = C >> A |
&= | 按位与后赋值 | C &= A 等于 C = C & A |
^= | 按位异或后赋值 | C ^= A 等于 C = C ^ A |
|= | 按位或后赋值 | C |= A 等于 C = C | A |
分支控制
-
单分支
- Go 的 if 还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了
package main import "fmt" func main() { if age := 20; age > 18 { fmt.Println("ok") fmt.Println(age) age1 := 22 fmt.Println(age1) } //fmt.Println(age) // 报错,不可引用if中定义的变量 //fmt.Println(age1) // 报错,不可引用if中定义的变量 }
-
swtich
- case后是一个表达式(即:常量值、变量、一个有返回值的函数等都可以)
- case后的各个表达式的值的数据类型,必须和switch的表达式数据类型一致
- case后面可以带多个表达式,使用逗号间隔。比如case 表达式1,表达式2
- case后面的表达式如果是常量(字面量),则要求不能重复
- case后面不需要带break,程序匹配到一个case后就会执行对应的代码块,然后退出switch,如果一个都匹配不到,则执行default
- default 语句不是必须的
- switch后也可以不带表达式,类似多个if-else分支来使用
- switch后也可以直接声明/定义一个变量,分号结束,不推荐
- switch穿透 fallthrough,如果在case语句块后增加fallthrough,则会继续执行下一个case,也叫switch穿透
- Type switch:switch语句还可以用于 type-switch来判断某个interface变量中实际指向的变量类型
package main import "fmt" func main() { var x interface{} var y = 10.0 x = y switch i := x.(type) { // type switch 语法 case nil: fmt.Printf("x 的类型:%T", i) case int: fmt.Printf("x 是 int 类型") case float64: fmt.Printf("x 是 float64 类型") case func(int) float64: fmt.Printf("x 是 func(int) float64 类型") case bool, string: fmt.Printf("x 是 bool 或 string 类型") default: fmt.Print("未知类型") } } // x 是 float64 类型
package main import "fmt" func test() string { return "8" } func main() { var a string = "8" switch { case a == "8": fmt.Println("1") case a == "9": fmt.Println("2") } switch b := "3"; a { case "1": fmt.Println("1") case "2", test(): fmt.Println("2") fallthrough case "3": fmt.Println(b) default: fmt.Println("0") } } // 输出 1 2 3
for range
传统for循环处理汉字时候会乱骂,需要进行切片处理(utf-8一个汉字对应3字节)
package main
import "fmt"
func main() {
var str string = "123123北京"
var str2 []rune = []rune(str)
fmt.Printf("%T \n", str2)
for i := 0; i < len(str2); i++ {
fmt.Printf("index=%d,str2=%c \n", i, str2[i])
}
fmt.Println("=======================")
for index, value := range str {
fmt.Printf("index=%d,value=%c\n", index, value)
}
}
/*
[]int32
index=0,str2=1
index=1,str2=2
index=2,str2=3
index=3,str2=1
index=4,str2=2
index=5,str2=3
index=6,str2=北
index=7,str2=京
*
break\continue标签的使用
break语句出现在多层嵌套的语句块中时,可以通过标签指明要终止的是哪一层语句块。
package main
import "fmt"
func main() {
for i := 1; i <=3; i++ {
fmt.Println("i=", i)
label: // 定义当前for循环的标签
for j := 1; j <= 2; j++ {
if (j > 1) {
break label // 跳出到该层标签
}
fmt.Println("j=", j)
}
}
}
continue 同理
package main
import "fmt"
func main() {
label: // 定义当前for循环的标签
for i := 1; i <=3; i++ {
fmt.Println("i=", i)
for j := 1; j <= 2; j++ {
if (j > 1) {
continue label // 直接继续下次循环
}
fmt.Println("j=", j)
}
}
}
包的注意事项和细节说明
- 在给一个文件打包时,该包对应一个文件夹,文件的包名通常和文件所在的文件夹名一致,一般为小写字母
- 当一个文件要使用其它包函数或变量时,需要先引入对应的包
- package指令在文件第一行,然后import指令
- 在import包时,路径从$GOPATH的src下开始,不用带src,编译器会自动从src下开始引入
- 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的public,这样才能跨包访问
- 在访问其它包函数时,其语法是包名.函数名
- 如果包名较长,Go支持给包取别名,注意细节:取别名后,原来的包名就不能用了
- 在同一包下,不能有相同的函数名,也不能有相同的全局变量名, 否则报重复定义
- 如果你要编译成一个可执行程序文件,就需要将这个包声明为main,即package main 这个是一个语法规范。如果你是写一个库,包名可以自定义
函数的注意事项
- 函数的形参列表可以是多个,返回值列表也可以是多个
- 形参列表和返回值列表的数据类型可以是值类型和引用类型
- 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似public,首字母小写,只能被本包文件使用,其它包文件不能使用,类似private
- 函数中的变量是局部的,函数外不生效
- 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
- 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。
- go函数不支持重载。
- 在go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
- 函数既然是一种数据类型,因此在go中,函数可以 作为形参,并且调用
package main
import "fmt"
func num(n1 int, n2 int) int {
return n1 + n2
}
func myNum(num func(int, int) int, n1 int, n2 int) int {
return num(n1, n2)
}
func main() {
n1 := 10
n2 := 20
funNum := num
res := myNum(funNum, n1, n2)
fmt.Println(res)
}
// 30
- 为了简化数据类型的定义,go支持自定义数据类型
- 基本语法:
type [自定义数据类型名] [数据类型]
- 基本语法:
package main
import "fmt"
func main() {
type myInt int // 在go中,myInt 和 int 虽然都是 int 类型,但是 go 认为 myInt 和 int 是两个不同的类型
var num myInt
num = 10
fmt.Printf("num,%T,%d", num, num)
}
// num,main.myInt,10
- 支持对函数返回值命名
package main
import "fmt"
func num(n1 int, n2 int) (sum int, sub int) {
sum = n1 + n2
sub = n1 - n2
return // 上面已经建立了对应关系,此处不需要指明返回值
}
func main() {
sum, sub := num(20, 10)
fmt.Println(sum) // 30
fmt.Println(sub) // 10
}
- 使用
_
标识符,忽略返回值 - go支持可变参数。如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后。
package main
import "fmt"
func num(args... int) int {
var sum int
for _, value := range args {
sum += value
}
return sum
}
func main() {
res := num(10, 20, 30, 40, 10)
fmt.Println(res) // 110
}
- 当函数的形参类型一样时,可以在后面写类型,前面的形参数据类型可以省略。
package main
import "fmt"
func num(n1, n2 float32) float32 {
return n1 + n2
}
func main() {
res := num(1, 2)
fmt.Println(res) // 3
}
init函数
每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被go运行框架调用,也就是说init会在main函数前被调用。
全局变量的运行在init函数之前。
package main
import "fmt"
func init() {
fmt.Println("init")
}
func main() {
fmt.Println("main")
}
// init
// main
匿名函数
package main
import "fmt"
func main() {
res := func (n1 int, n2 int) int {
return n1 + n2
}(10, 20)
fmt.Println(res)
mtFunc := func (n1 int, n2 int) int {
return n1 + n2
}
res2 := mtFunc(1, 1)
res3 := mtFunc(2, 1)
fmt.Println(res2)
fmt.Println(res3)
}
/*
30
2
3
*/
闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体。
package main
import "fmt"
func add() func(int) int {
var n int = 10 // 上下文n,受每次执行返回函数的影响
return func (x int) int {
n = n + x
return n
}
}
func main() {
f := add()
fmt.Println(f(1)) // 11
fmt.Println(f(2)) // 13
fmt.Println(f(3)) // 16
}
延时机制 defer
在函数中,程序员经常需要创建资源(比如:数据库连接、文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,go的设计者提供defer(延时机制)
package main
import "fmt"
func sum(n1 int, n2 int) int {
// 当执行到 defer 时候,暂时不执行,会将 defer 后面的语句"压入到独立的栈中",然后继续执行函数下一个语句
// 当函数执行完毕后,再从 defer 栈,按照先入后出的方式出栈执行
// 在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈
defer fmt.Println("n1=", n1) // n1 值不受下面修改影响,10
defer fmt.Println("n2=", n2) // n2 值不受下面修改影响,20
n1++
n2++
n3 := n1 + n2
fmt.Println("n3=", n3)
return n3
}
func main() {
n1 := 10
n2 := 20
res := sum(n1, n2)
fmt.Println("res=", res)
}
/*
n3= 32
n2= 20
n1= 10
res= 32
*/
字符串中常用的系统函数
package main
import (
"fmt"
"strconv"
"strings"
)
func main() {
var str string = "你好,世界"
// 统计字符串长度
strLen := len(str)
fmt.Println(strLen) // 15
// 字符串遍历
runeStr := []rune(str)
for index,value := range runeStr {
fmt.Printf("index=%d,value=%c ", index, value)
}
// index=0,value=你 index=1,value=好 index=2,value=, index=3,value=世 index=4,value=界
// 字符串转整数
n,_ := strconv.Atoi("12")
fmt.Println(n) // 12
// 整数转字符串
n1 := strconv.Itoa(12)
fmt.Println(n1) // 12
// 字符串转 []byte
s1 := []byte(str)
fmt.Println(s1) // [228 189 160 229 165 189 239 188 140 228 184 150 231 149 140]
// []byte 转字符串
s2 := string(s1)
fmt.Println(s2) // 你好,世界
// 十进制转二进制
s3 := strconv.FormatInt(123, 2)
fmt.Println(s3) // 1111011
// 查找子串是否在指定的字符串中
s4 := strings.Contains("hello word", "llo")
fmt.Println(s4) // true
// 统计一个字符串有几个指定的子串
s5 := strings.Count("ceheese", "e")
fmt.Println(s5) // 4
// 不区分大小写的字符串比较
s6 := strings.EqualFold("abc", "Abc")
fmt.Println(s6) // true
// 返回子串在字符串第一次出现的index值,如果没有返回-1
s7 := strings.Index("asdfasdfasdf", "d")
fmt.Println(s7) // 2
// 返回子串在字符串最后一次出现的index,如果没有返回-1
s8 := strings.LastIndex("asdfasdfasdf", "f")
fmt.Println(s8) // 11
// 将指定的子串替换成另外一个子串
s9 := strings.Replace("hello world", "world", "bndong", -1)
fmt.Println(s9) // hello bndong
// 按照指定的某个字符为分割标识,将一个字符串拆分成字符串数组
s10 := strings.Split("a,b,c,d,e", ",")
fmt.Println(s10) // [a b c d e]
// 按照指定的某个字符为拼接标识,将一个字符串数组拼接成字符串
e1 := strings.Join(s10, "_")
fmt.Println(e1) // a_b_c_d_e
// 重复字符串
e2 := strings.Repeat("aaa ", 3)
fmt.Println(e2) // aaa aaa aaa
// 将字符串的字母进行大小写转换
s11 := strings.ToLower("BNDong")
s12 := strings.ToUpper("BNDong")
fmt.Println(s11) // bndong
fmt.Println(s12) // BNDONG
// 将字符串的左右两边的空格去掉
s13 := strings.TrimSpace(" 123123 ")
fmt.Println(s13) // 123123
// 将字符串左右两边指定的字符去掉
s14 := strings.Trim("! 123123 !", "!")
fmt.Println(s14) // 123123
// 将字符串左边指定的字符去掉
s15 := strings.TrimLeft("! 123123 !", "!")
fmt.Println(s15) // 123123 !
// 将字符串右边指定的字符去掉
s16 := strings.TrimRight("! 123123 !", "!")
fmt.Println(s16) // ! 123123
// 判断字符串是否以指定的字符串开头
s17 := strings.HasPrefix("http://127.0.0.1", "http")
fmt.Println(s17) // true
// 判断字符串是否以指定的字符串结束
s18 := strings.HasSuffix("http://127.0.0.1", "0.1")
fmt.Println(s18) // true
}
时间日期函数
package main
import (
"fmt"
"time"
)
func main() {
// 当前时间
now := time.Now()
// 当前时间信息
fmt.Printf("年=%v\n", now.Year())
fmt.Printf("月=%v\n", now.Month())
fmt.Printf("月=%v\n", int(now.Month()))
fmt.Printf("日=%v\n", now.Day())
fmt.Printf("时=%v\n", now.Hour())
fmt.Printf("分=%v\n", now.Minute())
fmt.Printf("秒=%v\n", now.Second())
// 把时间格式化成字符串
fmt.Println(time.Now().Format("2006-01-02 15:04:05")) // 2006-01-02 15:04:05 中时间数字是固定的,不能修改
// 把日期字符串转化为时间
fmt.Println(time.Parse("01-02-2006", "06-17-2013"))
// 获取当前时间纳秒时间戳
fmt.Println(time.Now().UnixNano())
// 延时1000毫秒执行
time.Sleep(time.Millisecond * 1000)
fmt.Println("ok")
}
/*
年=2022
月=January
月=1
日=5
时=14
分=15
秒=31
2022-01-05 14:15:31
2013-06-17 00:00:00 +0000 UTC <nil>
1641363331933338000
ok
*/
错误处理
- go语言追求简洁优雅,所以,go语言不支持传统的try...cath...finally这处处理
- go中引入的处理方式为:defer,panic,recover
- 这几个异常的使用场景可以这么简单描述:go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理
package main
import "fmt"
func main() {
// 使用 defer + recover 来捕获和处理异常
defer func(){
err := recover()
if err != nil {
fmt.Println(err)
}
}()
n1 := 10
n2 := 0
res := n1 / n2
fmt.Println(res)
}
自定义错误
go 程序中,也支持自定义错误,使用errors.New 和 panic 内置函数
- errors.New("错误说明"),会返回一个error类型的值,表示一个错误
- panic内置函数,接收一个interface{}类型的值作为参数。可以接收error类型的变量,输出错误信息,并退出程序
package main
import (
"errors"
"fmt"
)
// 函数去读取以配置文件的信息
// 如果文件名传入不正确,我们就返回一个自定义错误
func readConf(name string) (err error) {
if name == "config.ini" {
// 读取...
return nil
}
// 返回一个自定义错误
return errors.New("读取文件错误...")
}
func main() {
err := readConf("config.ini")
if err != nil {
// 如果读取文件错误,就输出这个错误,并终止程序
/*
内建函数panic停止当前Go程的正常执行。当函数F调用panic时,F的正常执行就会立刻停止。
F中defer的所有函数先入后出执行后,F返回给其调用者G。G如同F一样行动,层层返回,直到该Go程中所有函数都按相反的顺序停止执行。
之后,程序被终止,而错误情况会被报告,包括引发该恐慌的实参值,此终止序列称为恐慌过程。
*/
panic(err)
}
fmt.Println("test")
}
/*
错误情况输出:
panic: 读取文件错误...
goroutine 1 [running]:
main.main()
*/
数组
- 数组是多个相同数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化
var arr []int
这时 arr 就是一个slice切片- 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用
- 数组创建后,如果没有赋值,有默认值
- 数值类型数组:默认值为0
- 字符串数组:默认值为“”
- bool数组:默认值为false
- 数组的下标是从0开始的
- 数组下标必须在指定范围内使用,否则报 panic:数组越界
- go的数组属值类型,在默认情况下是值传递,因此会进行值拷贝
- 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
package main
import "fmt"
func main() {
// 定义一个数组
var hens [6]int
// 给数组每个元素赋值
hens[0] = 10
hens[1] = 20
hens[2] = 30
hens[3] = 40
hens[4] = 50
hens[5] = 60
// 数组遍历
for index,value := range hens {
fmt.Printf("index=%d,value=%d,&value=%p\n", index, value, &hens[index])
}
// 定义数组的其它方式
var arr1 [3]int = [3]int {1,2,3}
var arr2 = [3]int {1,2,3}
var arr3 = [...]int {6,7,8}
var arr4 = [3]string {1:"tom", 2:"jack", 0:"marry"}
var arr5 = [...][2]int{
{1, 2},
{4, 5},
}
var arr6 [3][2]string = [...][2]string{
{"a", "b"},
{"c", "d"},
{"f", "g"},
}
fmt.Println(arr1)
fmt.Println(arr2)
fmt.Println(arr3)
fmt.Println(arr4)
fmt.Println(arr5)
fmt.Println(arr6)
}
/*
index=0,value=10,&value=0xc0000200c0
index=1,value=20,&value=0xc0000200c8
index=2,value=30,&value=0xc0000200d0
index=3,value=40,&value=0xc0000200d8
index=4,value=50,&value=0xc0000200e0
index=5,value=60,&value=0xc0000200e8
[1 2 3]
[1 2 3]
[6 7 8]
[marry tom jack]
[[1 2] [4 5]]
[[a b] [c d] [f g]]
*/
切片
- 切片的英文是slice
- 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制
- 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度len(slice)都一样
- 切片的长度是可以变化的,因此切片是一个动态变化数组
- 切片定义的基本语法:
var 变量名 []类型
- 切片初始化时
var slice = arr[startIndex:endIndex]
:从arr数组下标为startIndex,取到下标为endIndex的元素(不含arr[endIndex]) - 切片初始化时,仍然不能越界,范围在[0~len(arr)]之间,但是可以动态增长
var slice = arr[0:end]
可以简写var slice = arr[:end]
var slice = arr[start:len(arr)]
可以简写var slice = arr[:end]
var slice = arr[0:len(arr)]
可以简写var slice = arr[:]
- cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素
- 切片定义完成后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者make一个空间供切片来使用
- 切片可以继续切片
- 用
append
内置函数,可以对切片进行动态追加- 切片
append
操作的本质就是对数组扩容 - go底层会创建一下新的数组newArr(安装扩容后大小)
- 将slice原来包含的元素拷贝到新的数组newArr
- slice重新引用到newARR
- 注意newArr是在底层维护的,程序员不可见
- 切片
package main
import "fmt"
func main() {
// 演示切片的基本使用
intArr := [...]int{1,22,33,44,55} // 定义一个数组
fmt.Printf("intArr,%T,%v\n", intArr, intArr) // intArr,[5]int,[1 22 33 44 55]
// 声明/定义一个切片
// 1. slice 就是切片名
// 2. intArr[1:3] 表示 slice 引用到 intArr 这个数组
// 3. 引用 intArr 数组的起始下标为1,最后的下标为3(但是不包括3)
slice := intArr[1:3]
fmt.Printf("slice,%T,%v\n", slice, slice) // slice,[]int,[22 33]
fmt.Println("slice len,", len(slice)) // 元素个数:2
fmt.Println("slice cap,", cap(slice)) // 容量:4 (切片的容量是可以动态变化的)
// 0 -- 22
// 1 -- 33
for index,value := range slice {
fmt.Println(index, "--", value)
}
// slice 切片是引用,内存地址一致
fmt.Println(&(slice[0])) // 0xc0000200c8
fmt.Println(&(intArr[1])) // 0xc0000200c8
slice[0] = 88
fmt.Println(slice) // [88 33]
fmt.Println(intArr) // [1 88 33 44 55]
// 通过make来创建切片
/**
通过 make 和 intArr[1:3] 创建切片的区别
1. intArr[1:3] 是直接引用数组,这个数组是事先存在的,程序员是可见的
2. make 来创建切片,make 也会创建一个数组,是由切片在底层进行维护,程序员是看不见的
*/
var arr []int = make([]int, 4, 8)
arr[0] = 1
arr[1] = 2
arr[2] = 3
arr[3] = 4
fmt.Println(arr) // [1 2 3 4]
// 定义一个切片,直接就指定具体数组,使用原理类似make的方式
var strSlice []string = []string{"tom", "jack", "mary"}
fmt.Println(strSlice) // [tom jack mary]
fmt.Println(len(strSlice)) // 3
fmt.Println(cap(strSlice)) // 3
// 切片扩容
slice2 := []int{100,200,300,400}
// []int,[100 200 300 400],0xc00001a080
fmt.Printf("%T,%v,%p\n", slice2, slice2, slice2)
slice3 := append(slice2, 500, 600)
// []int,[100 200 300 400],0xc00001a080
fmt.Printf("%T,%v,%p\n", slice2, slice2, slice2)
// []int,[100 200 300 400 500 600],0xc00001e1c0
fmt.Printf("%T,%v,%p\n", slice3, slice3, slice3)
slice4 := append(slice3, slice2...)
// []int,[100 200 300 400 500 600 100 200 300 400],0xc0000c2000
fmt.Printf("%T,%v,%p\n", slice4, slice4, slice4)
// 切片拷贝
slice5 := []int{1,2,3,4,5}
// []int,[1 2 3 4 5],0xc0000b40c0
fmt.Printf("%T,%v,%p\n", slice5, slice5, slice5)
slice6 := make([]int, 10)
// []int,[0 0 0 0 0 0 0 0 0 0],0xc0000c4000
fmt.Printf("%T,%v,%p\n", slice6, slice6, slice6)
copy(slice6, slice5)
// []int,[1 2 3 4 5 0 0 0 0 0],0xc0000c4000
fmt.Printf("%T,%v,%p\n", slice6, slice6, slice6)
}
string 和 slice
package main
import "fmt"
func main() {
str := "hello.world"
// string底层是一个byte数组,因此string也可以进行切片处理
slice := str[6:]
fmt.Println(slice) // world
// string是不可变的,也就是说不能直接通过str[0]='z' 方式来修改字符串
// 如果需要修改字符串可以先将string -> []rune -> 修改 -> 重新转成string
strByte := []rune(str)
fmt.Println(strByte) // [104 101 108 108 111 46 119 111 114 108 100]
strByte[0] = 'z'
str2 := string(strByte)
fmt.Println(str2) // zello.world
}
map
- 基本语法:
var map变量名 map[keytype]valuetype
var a map[string]string
var a map[string]int
var a map[int]string
var a map[string]map[string]string
- 声明是不会分配内存的,初始化需要make,分配内存后才能赋值和使用
- map的curd操作
package main
import "fmt"
func main() {
var map1 map[int]string
map1 = make(map[int]string, 2)
map1[2] = "1"
map1[5] = "2"
map1[3] = "3"
map1[1] = "4"
// 新版本的map的key会进行排序,老版本里面的值是随机的
fmt.Println(map1) // map[1:4 2:1 3:3 5:2]
// 增加
map1[6] = "5"
fmt.Println(map1) // map[1:4 2:1 3:3 5:2 6:5]
// 修改
map1[6] = "6"
fmt.Println(map1) // map[1:4 2:1 3:3 5:2 6:6]
// 查询
val, findRes := map1[3]
fmt.Println(val)
fmt.Println(findRes) // 如果存在findRes为true否则为false
// 删除
delete(map1, 6)
fmt.Println(map1) // map[1:4 2:1 3:3 5:2]
}
- map是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的map
- map的容量达到后,再想map增加元素,会自动扩容,并不会发生panic,也就是说map能动态的增长键值对
- map的value也经常使用struct类型,更适合管理复杂的数据
struct 结构体
- 结构体定义:
type 结构体名称 struct {}
package main
import "fmt"
type Stu struct{
Name string
Age int
Func func(int, int) int
}
func main() {
// 定义结构体方式1
var s1 Stu
s1.Name = "dbnuo"
s1.Age = 20
s1.Func = func(a int, b int) int {
return a + b
}
fmt.Println(s1) //{dbnuo 20 0x108ba60}
fmt.Println(s1.Func(1, 2)) // 3
fmt.Printf("%T", s1.Func) // func(int, int) int
// 定义结构体方式2
s2 := Stu{"dbnuo", 21, func(a int, b int) int {
return a + b + 10
}}
fmt.Println(s2) // func(int, int) int{dbnuo 21 0x108bb60}
fmt.Println(s2.Func(1, 2)) // 13
// 定义结构体方式3
var s3 *Stu = new(Stu)
s3.Name = "aaa"
fmt.Println(*s3) // {aaa 0 <nil>}
// 定义结构体方式4
s4 := Stu{
Name: "dbnuo",
Age: 21, // 最后都要有逗号
}
fmt.Println(s4) // {dbnuo 21 <nil>}
}
-
结构体的所有字段在内存中是连续的
-
结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段
package main
import "fmt"
type A struct {
Num int
}
type B struct {
Num int
}
func main() {
var a A
var b B
a = A(b) // 可以进行转换,两个结构体内部完全一致
fmt.Println(a, b) // {0} {0}
}
- 结构体进行type重新定义(相当于取别名)。go认为是新的数据类型,但是相互间可以强转
package main
import "fmt"
type Student struct {
Name string
}
type Stu Student
func main() {
var stu1 Student
var stu2 Stu
// stu2 = stu1 // 不能直接赋值,数据类型不同
stu2 = Stu(stu1) // 可以相互间进行转换
fmt.Println(stu1, stu2)
}
- struct的每个字段上,可以写上一个tag,该tag可以通过反射机制获取,常见的使用场景就是序列化和反序列化
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string `json:"name"` // json:"name" 为 tag 标签
Age int `json:"age"`
}
func main() {
var stu Student
stu.Name = "dbnuo"
stu.Age = 10
stuJson, err := json.Marshal(stu)
fmt.Println(string(stuJson)) // {"name":"dbnuo","age":10}
fmt.Println(err)
}
方法
-
方法的声明
func (recevier type) methodName (参数列表) (返回值列表){ 方法体 return 返回值 }
- 参数列表:表示方法输入
- recevier type:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型
- receiver type:type可以是结构体,也可以其它的自定义类型
- receiver:就是type类型的一个变量(实例)
- 返回值列表:表示返回的值,可以多个
- 方法主体:表示为了实现某一功能代码块
- return语句不是必须的
-
go中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct
-
结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式
-
如希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
package main
import "fmt"
type Student struct {
Name string
}
/**
方法定义:值传递
*/
func (s Student) test() {
s.Name = "bndong"
fmt.Println(s.Name) // bndong
}
/**
方法定义:引用传递
*/
func (s *Student) test1() {
s.Name = "bndong"
fmt.Println(s.Name) // bndong
}
func main() {
var s Student
s.Name = "dbnuo"
s.test()
fmt.Println(s.Name) // 值拷贝,方法里不对外部变量影响,所以还是输出 dbnuo
s.test1()
fmt.Println(s.Name) // 因为是引用传递,方法里外部变量影响,所以输出 bndong
}
- 方法的访问范围控制的规则和函数一样。方法名首字母小写,只能在本包访问,方法名首字母大写,可以在本包和其它包访问
- 如果一个变量实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出
方法和函数的区别
- 调用方式不一样
- 函数的调用方式:函数名(实参列表)
- 方法的调用方式:变量.方法名(实参列表)
- 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
- 对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样可以
package main
import "fmt"
type Student struct {
Name string
}
// 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
func test1(s Student) {
fmt.Println(s.Name)
}
func test2(s *Student) {
fmt.Println(s.Name)
}
// 对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以
func (s Student) test3() {
s.Name = "bndong"
fmt.Println(s.Name)
}
func main() {
var s Student
s.Name = "dbnuo"
test1(s)
test2(&s)
s.test3()
fmt.Println(s.Name)
(&s).test3() // 从形式上是传入地址,但是本质上仍然是值拷贝
fmt.Println(s.Name)
}
面向对象:继承
在go中,如果一个struct嵌套了另外一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性
- 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
- 匿名结构体字段访问可以简化
- 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
- 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错
- 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
package main
import "fmt"
type Stu struct{
Name string
Age int
}
func (s *Stu) stuHello() {
fmt.Println("stu hello", s.Name)
}
type Stu1 struct {
Name string
Age int
stu1 int
sp float64
}
type Pupil struct {
Stu
asStu Stu1 // 有名结构体,下面的字段和方法必须通过名字访问,不能简化
Score int
sp float64
}
func main() {
p := Pupil{}
// p.Name = "a" // 当字段在两个匿名结构体中都存在时候,同时结构体本身没有同名的字段或方法,不能直接简化使用,会报错:ambiguous selector p.Name
p.asStu.Name = "a"
p.Stu.Age = 10
p.Score = 60
p.asStu.stu1 = 70 // 有名结构体里的字段,必须通过名字访问,不能简化
p.sp = 12.32 // 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问
/**
注意:stuHello方法在Stu的匿名结构体中,但是程序中并没有给Stu结构体中Name赋值
所以stuHello方法运行时候读取不到Name的值,结果输出:stu hello
如果赋值了其它匿名结构体中同名字段,对其它拥有同名字段的匿名结构体不会产生影响,数据空间具有独立性
*/
p.stuHello() // 输出:stu hello
fmt.Println(p) //{{ 10} {a 0 70 0} 60 12.32}
// 直接指定各个匿名结构体字段的值
p1 := Pupil{
Stu: Stu{
Name: "dbnuo",
Age: 12,
},
asStu: Stu1{
Name: "aaaa",
Age: 13,
stu1: 14,
sp: 11.11,
},
Score: 12,
sp: 12.11,
}
fmt.Println(p1) // {{dbnuo 12} {aaaa 13 14 11.11} 12 12.11}
}
类型断言
package main
import "fmt"
type Stu struct {
x int
y int
}
func main() {
var a interface{}
var s = Stu{1, 2}
a = s
var b Stu
b ,res := a.(Stu) // 类型断言,表示判断a是否指向Stu类型的变量,如果是就转成Stu类型的变量,如果是就转成Stu类型并赋值给b变量,否则报错
fmt.Println(a)
fmt.Println(b)
fmt.Println(res)
}
文件读取和写入
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
)
func main() {
filePath := "/Users/dbnuo/Development/Docker/dnmp/www/golang/test/src/go_code/project/main/test"
// 打开文件
file, err := os.OpenFile(filePath, os.O_RDWR|os.O_APPEND, os.ModePerm)
if err != nil {
fmt.Println("打开文件失败")
fmt.Println(err)
} else {
fmt.Println("打开文件成功")
// 使用延时机制关闭文件
defer func(file *os.File) {
var err = file.Close()
if err != nil {
fmt.Println("关闭文件失败")
fmt.Println(err)
} else {
fmt.Println("关闭文件成功")
}
}(file)
}
// 判断文件是否存在
_, err = os.Stat(filePath)
switch {
case err == nil:
fmt.Println("文件存在")
case os.IsNotExist(err):
fmt.Println("文件不存在")
default:
fmt.Println("未知错误")
}
// 读取文件
b := make([]byte, 10)
num, err := file.Read(b)
if err != nil {
fmt.Println("读取文件失败")
fmt.Println(err)
} else {
fmt.Println("读取文件字节数:", num)
fmt.Println("读取文件成功:", string(b))
}
// 一次性读取文件(不适合大文件)
file.Seek(0, 0) // 将指针置于文件开始
str, err := ioutil.ReadFile(filePath)
if err != nil {
fmt.Println("一次性读取文件失败")
fmt.Println(err)
} else {
fmt.Println("一次性读取文件成功:")
fmt.Printf("%v\n", string(str))
}
// 缓冲读取内容
file.Seek(0, 0) // 将指针置于文件开始
reader := bufio.NewReader(file)
fmt.Println("缓冲读取文件:")
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
fmt.Printf("%v",str)
break
}
fmt.Printf("%v", str)
}
fmt.Println()
// 写入文件内容
_, err = file.WriteString("\nhahahaha")
if err != nil {
fmt.Println("写入文件失败")
fmt.Println(err)
} else {
fmt.Println("写入文件成功")
}
// 带缓存写入文件内容
writer := bufio.NewWriter(file)
_, err = writer.WriteString("\nlalalala")
if err != nil {
fmt.Println("带缓存写入文件失败")
fmt.Println(err)
} else {
fmt.Println("带缓存写入文件成功")
// 因为writer是带缓存的,因此在调用WriteString方法时,其实内容是先写入到缓存中的,
// 所以需要调用Flush方法,将缓存的数据真正写入到文件中,否则文件会丢失数据
writer.Flush()
}
}
序列化和反序列化
package main
import (
"encoding/json"
"fmt"
)
type Stu struct {
Name string
Age int
}
func main() {
// 结构体 json 序列化
stu := Stu{
Name: "bndong",
Age: 22,
}
data, err := json.Marshal(stu)
fmt.Println(string(data))
fmt.Println(err)
// map json 序列化
var a map[string]interface{}
a = make(map[string]interface{}, 2)
a["name"] = "张三"
a["age"] = 12
a["arr"] = []int{1,2,3,4}
data1, err1 := json.Marshal(a)
fmt.Println(string(data1))
fmt.Println(err1)
var jsonData interface{}
// 反序列化
json.Unmarshal(data, &jsonData)
fmt.Printf("%T,%v\n", jsonData, jsonData) // map[string]interface {},map[Age:22 Name:bndong]
json.Unmarshal(data1, &jsonData)
fmt.Printf("%T,%v\n", jsonData, jsonData) // map[string]interface {},map[age:12 arr:[1 2 3 4] name:张三]
}
单元测试
- 测试用例文件名必须以
_test.go
结尾 - 测试用例函数必须以Test开头,一般来说就是Test+被测试的函数名
- 一个测试用例文件中,可以有多个测试用例函数
- 运行测试用例指令:
go test
如果运行正确,无日志,错误时,会输出日志go test -v
运行正确或是错误,都输出日志
- 当出现错误时,可以使用
r.Fatalf
来格式化输出错误信息,并退出程序 t.Logf
方法可以输出相应的日志- 测试用例函数,并没有放在main函数中,也执行了,这就是测试用例的方便之处
- PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败
- 测试单个文件,一定要带上被测试的原文件:
go test -v cal_test.go cal.go
- 测试单个方法:
go test -v -test.run TestAddUpper
package main
import "testing"
func TestNum(t *testing.T) {
res := num(10)
if res != 91 {
t.Fatalf("执行测试错误,期望91,实际输出%v", res)
}
t.Logf("执行成功")
}
goroutine
- 进程和线程说明
- 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单元,它是比进程更小的能独立运行的基本单位
- 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
- 一个程序至少有一个进程,一个进程至少有一个线程
- 并发和并行
- 多线程程序在单核上运行,就是并发
- 因为是一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发
- 多线程程序在多核上运行,就是并行
- 因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行
- 多线程程序在单核上运行,就是并发
- go协程和go主线程
- go主线程(也可以理解成进程):一个go线程上,可以起多个协程。协程是轻量级的线程
- go协程的特点
- 有独立的栈空间
- 共享程序堆空间
- 调度由用户控制
- 协程是轻量级的线程