Golang基础知识点整理

两种执行流程的方式区别

  1. 如果我们先编译生成了可执行文件,那么我们可以将该可执行文件拷贝到没有go开发环境的机器上,仍然可以运行
  2. 如果我们是直接go run go 源代码,那么如果要在另外一个机器上这么运行,也需要go开发环境,否则无法执行
  3. 在编译时,编译器会将程序运行依赖的库文件包含在可执行文件中,所以,可执行文件变大了很多

程序开发注意事项

  1. go源文件以“go”为扩展名
  2. go应用程序的执行入口时main()函数
  3. go语言严格区分大小写
  4. go方法由一条条语句构成,每个语句后不需要分号(go语言会在每行后自动加分号),这也体现出golang的简洁性
  5. go编译器是一行行进行编译的,因此我们一行就写一条语句,不能把多条语句写在同一个,否则报错
  6. go语言定义的变量或者import的包如果没有使用到,代码不能编译通过
  7. 大括号都是成对出现的,缺一不可

程序中"\r"回车的效果

\r 回车,从当前行的最前面开始输出,覆盖掉前面相同长度的内容。

例如:

fmt.Println("hello word\rhhhhh")

输出:

hhhhh word

程序的注释

行注释:// 块注释:/* 注释内容 */

变量使用注意事项

  1. 变量表示内存中的一个存 储区域

  2. 该区域有自己的名称(变量名)和类型(数据类型)

  3. Golang变量使用的三种方式

    1. 第一种:指定变量类型,声明后若不赋值,使用默认值
    2. 第二种:根据值自行判定变量类型(类型推导)
    3. 第三种:省略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
    
  4. 多变量声明

    在编程中,有时我们需要一次性声明多个变量,Golang也提供这样的语法

程序中“+“号的使用

  1. 当左右两边都是数值型时,则做加法运算
  2. 当左右两边都是字符串,则做字符串拼接

变量的数据类型

  • 基本数据类型
    • 数值型
      • 整数类型(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
*/

指针

  1. 基本数据类型,变量存的就是值,也叫值类型
  2. 获取变量的地址,用&
  3. 指针的类型,指针变量存的是一个地址,这个地址指向的空间存的才是值
  4. 获取指针类型所指向的值,使用*
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)
		}
	}
}

包的注意事项和细节说明

  1. 在给一个文件打包时,该包对应一个文件夹,文件的包名通常和文件所在的文件夹名一致,一般为小写字母
  2. 当一个文件要使用其它包函数或变量时,需要先引入对应的包
  3. package指令在文件第一行,然后import指令
  4. 在import包时,路径从$GOPATH的src下开始,不用带src,编译器会自动从src下开始引入
  5. 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的public,这样才能跨包访问
  6. 在访问其它包函数时,其语法是包名.函数名
  7. 如果包名较长,Go支持给包取别名,注意细节:取别名后,原来的包名就不能用了
  8. 在同一包下,不能有相同的函数名,也不能有相同的全局变量名, 否则报重复定义
  9. 如果你要编译成一个可执行程序文件,就需要将这个包声明为main,即package main 这个是一个语法规范。如果你是写一个库,包名可以自定义

函数的注意事项

  1. 函数的形参列表可以是多个,返回值列表也可以是多个
  2. 形参列表和返回值列表的数据类型可以是值类型和引用类型
  3. 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似public,首字母小写,只能被本包文件使用,其它包文件不能使用,类似private
  4. 函数中的变量是局部的,函数外不生效
  5. 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值。
  6. 如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。
  7. go函数不支持重载。
  8. 在go中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
  9. 函数既然是一种数据类型,因此在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
  1. 为了简化数据类型的定义,go支持自定义数据类型
    1. 基本语法: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
  1. 支持对函数返回值命名
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
}
  1. 使用_标识符,忽略返回值
  2. 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
}
  1. 当函数的形参类型一样时,可以在后面写类型,前面的形参数据类型可以省略。
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
*/

错误处理

  1. go语言追求简洁优雅,所以,go语言不支持传统的try...cath...finally这处处理
  2. go中引入的处理方式为:defer,panic,recover
  3. 这几个异常的使用场景可以这么简单描述: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 内置函数

  1. errors.New("错误说明"),会返回一个error类型的值,表示一个错误
  2. 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()
*/

数组

  1. 数组是多个相同数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化
  2. var arr []int 这时 arr 就是一个slice切片
  3. 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用
  4. 数组创建后,如果没有赋值,有默认值
    1. 数值类型数组:默认值为0
    2. 字符串数组:默认值为“”
    3. bool数组:默认值为false
  5. 数组的下标是从0开始的
  6. 数组下标必须在指定范围内使用,否则报 panic:数组越界
  7. go的数组属值类型,在默认情况下是值传递,因此会进行值拷贝
  8. 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
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]]
*/

切片

  1. 切片的英文是slice
  2. 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制
  3. 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度len(slice)都一样
  4. 切片的长度是可以变化的,因此切片是一个动态变化数组
  5. 切片定义的基本语法:var 变量名 []类型
  6. 切片初始化时 var slice = arr[startIndex:endIndex]:从arr数组下标为startIndex,取到下标为endIndex的元素(不含arr[endIndex])
  7. 切片初始化时,仍然不能越界,范围在[0~len(arr)]之间,但是可以动态增长
    1. var slice = arr[0:end] 可以简写 var slice = arr[:end]
    2. var slice = arr[start:len(arr)] 可以简写 var slice = arr[:end]
    3. var slice = arr[0:len(arr)] 可以简写 var slice = arr[:]
  8. cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素
  9. 切片定义完成后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者make一个空间供切片来使用
  10. 切片可以继续切片
  11. append内置函数,可以对切片进行动态追加
    1. 切片append操作的本质就是对数组扩容
    2. go底层会创建一下新的数组newArr(安装扩容后大小)
    3. 将slice原来包含的元素拷贝到新的数组newArr
    4. slice重新引用到newARR
    5. 注意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

  1. 基本语法:var map变量名 map[keytype]valuetype
    1. var a map[string]string
    2. var a map[string]int
    3. var a map[int]string
    4. var a map[string]map[string]string
  2. 声明是不会分配内存的,初始化需要make,分配内存后才能赋值和使用
  3. 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]
}
  1. map是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的map
  2. map的容量达到后,再想map增加元素,会自动扩容,并不会发生panic,也就是说map能动态的增长键值对
  3. map的value也经常使用struct类型,更适合管理复杂的数据

struct 结构体

  1. 结构体定义: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>}
}
  1. 结构体的所有字段在内存中是连续的

  2. 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段

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}
}
  1. 结构体进行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)
}
  1. 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)
}

方法

  1. 方法的声明

    func (recevier type) methodName (参数列表) (返回值列表){
      方法体
      return 返回值
    }
    
    1. 参数列表:表示方法输入
    2. recevier type:表示这个方法和type这个类型进行绑定,或者说该方法作用于type类型
    3. receiver type:type可以是结构体,也可以其它的自定义类型
    4. receiver:就是type类型的一个变量(实例)
    5. 返回值列表:表示返回的值,可以多个
    6. 方法主体:表示为了实现某一功能代码块
    7. return语句不是必须的
  2. go中的方法是作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct

  3. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式

  4. 如希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理

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
} 
  1. 方法的访问范围控制的规则和函数一样。方法名首字母小写,只能在本包访问,方法名首字母大写,可以在本包和其它包访问
  2. 如果一个变量实现了String()这个方法,那么fmt.Println默认会调用这个变量的String()进行输出

方法和函数的区别

  1. 调用方式不一样
    1. 函数的调用方式:函数名(实参列表)
    2. 方法的调用方式:变量.方法名(实参列表)
  2. 对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然
  3. 对于方法,接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样可以
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嵌套了另外一个匿名结构体,那么这个结构体可以直接访问匿名结构体的字段和方法,从而实现了继承特性

  1. 结构体可以使用嵌套匿名结构体所有的字段和方法,即:首字母大写或者小写的字段、方法,都可以使用
  2. 匿名结构体字段访问可以简化
  3. 当结构体和匿名结构体有相同的字段或者方法时,编译器采用就近访问原则访问,如希望访问匿名结构体的字段和方法,可以通过匿名结构体名来区分
  4. 结构体嵌入两个(或多个)匿名结构体,如两个匿名结构体有相同的字段和方法(同时结构体本身没有同名的字段和方法),在访问时,就必须明确指定匿名结构体名字,否则编译报错
  5. 嵌套匿名结构体后,也可以在创建结构体变量(实例)时,直接指定各个匿名结构体字段的值
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:张三]

}

单元测试

  1. 测试用例文件名必须以 _test.go 结尾
  2. 测试用例函数必须以Test开头,一般来说就是Test+被测试的函数名
  3. 一个测试用例文件中,可以有多个测试用例函数
  4. 运行测试用例指令:
    1. go test 如果运行正确,无日志,错误时,会输出日志
    2. go test -v 运行正确或是错误,都输出日志
  5. 当出现错误时,可以使用 r.Fatalf 来格式化输出错误信息,并退出程序
  6. t.Logf 方法可以输出相应的日志
  7. 测试用例函数,并没有放在main函数中,也执行了,这就是测试用例的方便之处
  8. PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败
  9. 测试单个文件,一定要带上被测试的原文件:go test -v cal_test.go cal.go
  10. 测试单个方法: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协程的特点
    • 有独立的栈空间
    • 共享程序堆空间
    • 调度由用户控制
    • 协程是轻量级的线程
posted @ 2022-09-26 15:22  BNDong  阅读(1515)  评论(2编辑  收藏  举报