Skip to content

Golang 官方网站 https://golang.org Golang 中文网 在线标准库文档: https://studygolang.com/pkgdoc

变量使用

声明变量

go
// 定义全局变量
var num1 = 100
var num2 = 200
var num3 = 300

// 一次性声明全局变量
var (
	num4 = 100
	num5 = 200
	num6 = 300
)

/**
*@Author: Sunjianwang
*@Description: Golang变量使用方式
*@CreateTime: 2022-07-03 14:59:00
**/
func learnVariable() {

	//方式一:指定变量类型,声明后若不赋值,使用默认值
	//int的默认值是0
	var i int
	fmt.Println("i=", i)

	//方式二:根据值自行判定变量类型(类型推导)
	var num = 11.11
	fmt.Println("num=", num)
	fmt.Printf("%T\n", num)

	//方式三:省略var, 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误
	//等价于:
	//var name string
	//name = "tom"
	name := "Tom"
	fmt.Println("name=", name)

	//一次性声明多个变量
	var n1, n2, n3 = 1, 2, "Tom"
	fmt.Println(n1, n2, n3)

	n4, n5, n6 := 3, 4, "Golang"
	fmt.Println(n4, n5, n6)

	//打印全局变量
	fmt.Println("全局变量", num4, num5, num6)
}

数据类型

基本数据类型

  • 数值型(int):

    • 整数类型:int,int8,int16,int32,int64,unit,unit8,unit16,unit32,unit64
    • 浮点类型:float32,float64
  • 字符型:(没有专门的字符型,使用byte来)

  • 布尔型(bool):

  • 字符串(string):golang将字符串算到基本数据类型中

基本数据类型使用

整数类型

类型有无符号占用存储空间表数范围备注
int32位系统4个字节 64位系统8个字节-231 ~ 231-1 -263 ~ 263-1
int81字节-128 ~ 127
int162字节-215 ~ 215-1
int324字节-231 ~ 231-1
int648字节-263 ~ 263-1
uint32位系统4个字节 64位系统8个字节0 ~ 232-1 0 ~ 264-1
uint81字节0 ~ 255
uint162字节0 ~ 216-1
uint324字节0 ~ 232-1
uint648字节0 ~ 264-1
rune与int32一样-231 ~ 231-1等价int32,表示一个Unicode码
byte与uint8一样0 ~ 255当要存储字符时,选用byte
go
/**
*@Project: learnGo
*@Package: main
*@Author: Sunjianwang
*@CreateTime: 2022-07-03 15:19:44
*@Description: 数据类型
*@Version: 1.0
 */

package main

import (
	"fmt"
	"unsafe"
)

/**
*@Author: Sunjianwang
*@Description: 数据类型:分为基本数据类型、派生/复杂数据类型
*@CreateTime: 2022-07-03 15:19:52
**/
func learnDataType() {

	var i int = 10
	fmt.Println(i)

	//测试int8范围 -128~127 赋值-129报错
	var j int8 = -128
	fmt.Println("j=", j)

	//测试unit8范围 0~255 赋值负数,大于255报错
	var k uint8 = 0
	fmt.Println("k=", k)

	//int, uint, rune, byte的使用
	var a int = 8900
	fmt.Println(a)
	fmt.Printf("a的类型%T\n", a)

	var b rune = 128
	fmt.Println(b)

	var c byte = 'c'
	fmt.Printf("c=%c\n", c)

	//整型的使用细节
	var n1 = 100 //问:n1是什么类型
	//查看某个变量的数据类型
	//fmt.Printf()用作格式化输出
	fmt.Printf("%T\n", n1)

	//查看某个变量的占用字节大小和数据类型(使用较多)
	var n2 int64 = 1000
	fmt.Printf("n2的类型是 %T,占用的字节数 %d\n", n2, unsafe.Sizeof(n2))
}

浮点类型

类型占用存储空间表数范围
单精度float324字节-3.403E38 ~ 3.403E38
双精度float648字节-1.798E308 ~ 1.798E308

说明:

  1. 关于浮点数在机器中存放形式的简单说明:浮点数=符号位+指数位+尾数位。说明浮点数都是有符号的
  2. 尾数部分可能丢失,造成精度损失。

使用细节:

  1. Golang浮点类型有固定的范围和字段长度,不受OS的影响
  2. Golang的浮点型默认声明为float64类型
  3. 浮点型常量有两种表示形式: 十进制数形式,如:5.12 科学计数法形式,如:5.1234e2 = 5.1234 * 10^2
  4. 通常情况下应使用float64,因为它更精准
go
//
//基本数据类型:浮点型
//@author: Sunjianwang
//
func learnFloatType() {
	fmt.Println("*********浮点型*********")

	var price float32 = 89.123
	fmt.Println("price=", price)

	//浮点数精度损失,打印结果:num1= -123.00009 num2= -123.0000901
	var num1 float32 = -123.0000901
	var num2 float64 = -123.0000901
	fmt.Println("num1=", num1, "num2=", num2)

	//十进制数形式,打印结果:num3= 5.12 num4= 0.12
	num3 := 5.12
	num4 := .12
	fmt.Println("num3=", num3, "num4=", num4)

	//科学计数法形式,打印结果:num5= 512.34 num6= 512.34 num7= 0.051234
	num5 := 5.1234e2
	num6 := 5.1234e2
	num7 := 5.1234e-2
	fmt.Println("num5=", num5, "num6=", num6, "num7=", num7)
}

字符类型

Golang没有专门的字符类型,如果要存储单个字符,可以用byte来保存

说明:

  1. 如果保存的字符在ASCII表中,可以使用byte保存
  2. 如果保存的字符码值大于255,可以考虑用int来保存
  3. 可以使用格式化输出(%c)打印保存的字符

使用细节:

  1. 字符常量是用单引号('')括起来的单个字符。例如:var c1 byte = 'a' var c2 int = '中' var c3 byte = '9'
  2. Go中允许使用转义字符'\’来将其后的字符转变为特殊字符型常量。例如:var c3 char = ‘\n’ // '\n'表示换行符
  3. Go语言的字符使用UTF-8编码,英文字母1个字节 汉字3个字节
  4. 在Go中,字符的本质是一个整数,直接输出是该字符对应的UTF-8编码的码值。
  5. 可以直接给某个变量赋一个数字,然后按格式化输出时%c,会输出该数字对应的unicode字符
  6. 字符类型是可以进行运算的,相当于一个整数,因为它都对应有Unicode码
go
//
//基本数据类型:字符型
//@author: Sunjianwang
//
func learnCharType() {

	var c1 byte = 'a'
	var c2 byte = '0'
	//当我们直接输出byte值,就是输出了对应的ASCII码
	fmt.Println("c1=", c1)
	fmt.Println("c2=", c2)
	//格式化输出
	fmt.Printf("c1=%c\n", c1)
	fmt.Printf("c2=%c\n", c2)

	//var c3 byte = '北' //overflow溢出
	//可以用int
	var c3 int = ''
	fmt.Printf("c3=%c\n", c3)

	//数字转化为对应的unicode字符
	var c4 int = 22269
	fmt.Printf("c4=%c\n", c4)

	//字符类型可以运算,相当于一个整数,运算时按照码值
	var n1 = 10 + 'a'
	fmt.Println("n1=", n1)
	fmt.Printf("n1=%c\n", n1)
}

布尔类型

  1. 布尔类型也称bool类型,bool类型只允许取值true和false
  2. bool类型占用一个字节
  3. bool 类型适于逻辑运算,一般用于程序流程控制
go
//
//基本数据类型:布尔型
//@author: Sunjianwang
//
func learnBoolean() {
	var b = false
	fmt.Println(b)
	//注意事项
	//1. bool类型占用存储空间是1个字节
	fmt.Println("b的占用空间 = ", unsafe.Sizeof(b))

	//2. bool类型只能取true或false
	//b = 1 ×
}

字符串类型

字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用UTF-8编码标识Unicode文本

细节

  1. Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本,这样 Golang 统一使用 UTF-8 编码,中文乱码问题不会再困扰程序员

  2. 字符串一旦赋值了,字符串就不能修改了:在 Go 中字符串是不可变的,可以重新赋值

  3. 字符串的两种表示形式

    1. 双引号, 会识别转义字符
    2. 反引号,以字符串的原生形式输出,包括换行和特殊字符,可以实现防止攻击、输出源代码等效果
  4. 字符串拼接方式:使用+号拼接

  5. 当一个拼接的操作很长时,可以分行写,每行的加号要留在上一行,因为go在编译时如果行尾没有加号会在行尾加一个 ; 号, + 号留在行首会报错

go
//
//基本数据类型:字符串类型
//@author: Sunjianwang
//
func learnString() {
	//基本使用
	var str string = "河北省张家口市"
	fmt.Println(str)

	//字符串一旦赋值,字符串就不能修改了:在Go中字符串是不可变的
	str1 := "hello"
	//str1[0] = 'a' //不能修改
	str1 = "hello world" //可以重新赋值
	fmt.Println(str1)

	//字符串的两种表示形式
	//1. 双引号, 会识别转义字符
	//2. 反引号,以字符串的原生形式输出,包括换行和特殊字符,
	//可以实现防止攻击、输出源代码等效果
	str2 := "abc\nabc"
	fmt.Println("识别转义字符:", str2)
	str2 = `
//
//基本数据类型:布尔型
//@author: Sunjianwang
//
func learnBoolean() {
	var b = false
	fmt.Println(b)
	//注意事项
	//1. bool类型占用存储空间是1个字节
	fmt.Println("b的占用空间 = ", unsafe.Sizeof(b))

	//2. bool类型只能取true或false
	//b = 1 ×
}`
	fmt.Println("以字符串原生的格式输出:", str2)

	//字符串拼接方式
	str3 := "hello " + "world"
	fmt.Println("字符串拼接:", str3)

	//当一个拼接的操作很长时,可以分行写
	//每行的加号要留在上一行,因为go在编译时如果行尾没有加号会在行尾加一个;号,
	//+号留在行首会报错
	str4 := "hello " +
		"world " +
		"Haha" +
		"!"
	fmt.Println(str4)
}

基本数据类型默认值一览表

在 go 中,数据类型都有一个默认值,当程序员没有赋值时,就会保留默认值,在 go 中,默认值又叫零值。

类型有无符号
整型0
浮点型0
字符串""
布尔类型false

基本数据类型转换

Golang 和 java / c 不同,Go 在不同类型的变量之间赋值时需要显式转换。也就是说 Golang 中数据类型不能自动转换

基本语法

表达式T(v):将v转化为T类型

go
//
//基本数据类型转换:T(v)
//@author: Sunjianwang
//
func transType() {
	var i int = 100

	//将i转化为float
	var iFloat float32 = float32(i)
	fmt.Printf("i=%v,%T;iFloat=%v,%T \n", i, i, iFloat, iFloat)

	//转换中,比如将 int64	转成 int8 【-128---127】 ,编译时不会报错,
	//只是转换的结果是按溢出处理,和我们希望的结果不一样。 
	//因此在转换时,需要考虑范围
	var num1 = 999999
	var num2 = int8(num1)
	fmt.Println(num2) //打印63
}

细节

  1. Go 中,数据类型的转换可以是从 表示范围小-->表示范围大,也可以 范围大--->范围小
  2. 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化
  3. 在转换中,比如将 int64 转成 int8 【-128---127】 ,编译时不会报错,只是转换的结果是按溢出处理,和我们希望的结果不一样。在转换时,需要考虑范围

注意

go
	var n1 int32 = 12
	var n3 int8
	var n4 int8
	
	n3 = int8(n1) + 127 //【编译可以通过,但是结果不是127 + 12】
	n4 = int8(n1) + 128 //【编译不通过,int8范围是-128~127,128超过了int8最大赋值范围】

基本数据类型转string

go
//
//基本数据类型转string
//@author: Sunjianwang
//
func tansToString() {

	var num1 int8 = 99
	var num2 float64 = 123.456
	var b bool = true
	var c byte = 'h'
	var str string

	//推荐方式1:fmt.Sprintf("%参数",	表达式)
	str = fmt.Sprintf("%d", num1)
	fmt.Println("int转string:", str)

	str = fmt.Sprintf("%f", num2)
	fmt.Println("float64转string:", str)

	str = fmt.Sprintf("%t", b)
	fmt.Println("bool转string:", str)

	str = fmt.Sprintf("%c", c)
	fmt.Println("byte转string:", str)

	//方式2:strconv包中的函数
	str = strconv.FormatInt(int64(num1), 10)
	fmt.Println("int64转string:", str)

	str = strconv.FormatFloat(num2, 'f', 3, 64)
	fmt.Println("Float64转string:", str)

	str = strconv.FormatBool(b)
	fmt.Println("Bool转string:", str)
}

指针

基本使用

go
//
//指针基本使用
//@author: Sunjianwang
//
func pointer() {
	//基本数据类型在内存布局
	var num = 100

	var ptr *int = &num

	fmt.Println("ptr是一个地址,指向一个数值:", ptr)
	fmt.Println("*ptr获取ptr所指向的数值:", *ptr)
	fmt.Println("&ptr获取ptr变量的地址:", &ptr)
	fmt.Println("*(&ptr)获取ptr变量所指向数值的地址:", *(&ptr))
	fmt.Println("*(*(&ptr))获取ptr变量所指向的地址的值:", *(*(&ptr)))

	var a = 300
	var b = 400

	var ptr1 *int = &a
	*ptr1 = 100
	ptr1 = &b
	*ptr1 = 200

	fmt.Printf("a=%v,b=%v,ptr=%v", a, b, *ptr1) //a=100,b=200,ptr=200
}

值类型和引用类型

值类型 基本数据类型:int系列,float系列,bool,string;数组和结构体stuct 引用类型 指针、slice 切片、map、管道 chan、interface 等都是引用类型

值类型和引用类型区别

  1. 值类型:变量直接存储值,内存通常在栈中分配
  2. 引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收

标识符

标识符概念

  1. Golang 对各种变量、方法、函数等命名时使用的字符序列称为标识符
  2. 凡是自己可以起名字的地方都叫标识符

标识符命名规则

  1. 由 26 个英文字母大小写,0-9 ,_ 组成
  2. 数字不可以开头。
  3. Golang 中严格区分大小写。
go
//Golang中严格区分大小写
	var num int = 10
	var Num int = 20
	fmt.Printf("Golang中严格区分大小写:num=%v,Num=%v", num, Num)
  //Golang中严格区分大小写:num=10,Num=20
  1. 标识符不能包含空格
  2. 下划线"_"本身在Go中是一个特殊的标识符,称为空标识符。可以代表任何其它的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用
  3. 不能以系统保留关键字作为标识符(一共有 25 个),比如 break,if 等等

标识符命名规范

  1. 包名:保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和标准库不要冲突
  2. 变量名、函数名、常量名:采用驼峰法
  3. 如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用 ( 注:可以简单的理解成,首字母大写是公开的,首字母小写是私有的) ,在 golang 没有public , private 等关键字

运算符

算数运算符

取整、取模、自增自减

go
//
//取整、取模、自增自减
//@author: Sunjianwang
//
func calculationTest() {

	//除法 /
	//如果运算的都是整数,那么除后保留整数部分,和接收的变量无关
	fmt.Println(10 / 4)
	var n1 float64 = 10 / 4
	fmt.Println(n1)

	//如果我们希望保留小数部分,则需要有浮点数参与运算
	var n2 float64 = 10.0 / 4
	fmt.Println(n2)

	//模 %
	//公式:a % b = a - a / b * b
	fmt.Println("10%3=", 10%3)
	fmt.Println("-10%3=", -10%3)   // = -10  - (-10) / 3 * 3 = -10 - (-9) = -1
	fmt.Println("10%-3=", 10%-3)   // = 10 - 10 / (-3) * -3 = 1
	fmt.Println("-10%-3=", -10%-3) // = -10 - (-10) / (-3) * (-3) = -1

	var num = 1
	num++
	fmt.Println(num)
	num--
	fmt.Println(num)
}

使用细节

  1. 对于除号 "/",它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃小数部分。 例如: x := 19/5 ,结果是3
  2. 当对一个数取模时,可以等价 a%b=a-a/b*b , 这样我们可以看到 取模的一个本质运算。
  3. Golang 的自增自减只能当做一个独立语言使用时,不能这样使用
  4. Golang 的++ 和 -- 只能写在变量的后面,不能写在变量的前面,即:只有 a++ a-- 没有 ++a --a

关系运算符

运算符运算
==等于
!=不等于
>大于
<小于
>=大于等于
<=小于等于

逻辑运算符

运算符描述
&&逻辑与(短路与):全真,结果为true,否则为false(如果第一个条件为false,则后面的条件不会进行判断,直接返回false)
||逻辑或(短路或):有一个为真则结果为true,全假则结果为false(如果第一个条件为true,则后面的条件不会进行判断,直接返回false)
!逻辑非:条件为true,则逻辑非为false

赋值运算符

运算符描述
=简单的赋值运算符
+=相加后赋值
-=相减后赋值
*=相乘后赋值
/=整除后赋值
%=求余后赋值
<<=左移后赋值
>>=右移后赋值
&=按位与,然后复制
^=按位异或,然后赋值
!=按位或,然后赋值
go
	//不使用中间变量,将a、b两个变量的值互换
	var a = 10
	var b = 20

	a = a + b //
	b = a - b // b = a + b - b ==> b = a
	a = a - b // a = a + b - b = a + b - a ==> a =  b
	fmt.Printf("a=%v,b=%v", a, b)

运算符执行顺序

大体顺序

  1. 括号,++, --
  2. 单目运算
  3. 算术运算符
  4. 移位运算
  5. 关系运算符
  6. 位运算符
  7. 逻辑运算符
  8. 赋值运算符
  9. 逗号

流程控制

分支

go
//
//流程控制
//@author: Sunjianwang
//
func processTest() {
	var num int

	fmt.Scanln(&num)

	if num > 18 {
		fmt.Println("已经成年了!")
	} else { //else不能换行,否则编译不通过
		fmt.Println("还未成年!")
	}

	var input string

	fmt.Scanln(&input)

	//go中没有break,默认一个case匹配一次
	switch input {
	case "abc", "bbb": //可以有多个表达式,
		fmt.Println(input)
	case "aaa":
		fmt.Println("aaa匹配")
		fallthrough //switch穿透,会继续执行下一个 case
	default:
		fmt.Println("不是合法值!")
	}

	//switch后面可以不带表达式
	switch {
	case num > 100:
		fmt.Println("输入值大于100")
	case num > 50 && num <= 99:
		fmt.Println("输入值在50~100")
	default:
		fmt.Println("输入值是", num)
	}
}

使用细节

  1. case/switch 后是一个表达式( 即:常量值、变量、一个有返回值的函数等都可以)
  2. case 后的各个表达式的值的数据类型,必须和 switch 的表达式数据类型一致
  3. case 后面可以带多个表达式,使用逗号间隔。比如 case 表达式 1,表达式 2...
  4. case 后面的表达式如果是常量值(字面量),则要求不能重复
  5. case 后面不需要带break , 程序匹配到一个 case 后就会执行对应的代码块,然后退出 switch,如果一个都匹配不到,则执行 default
  6. default 语句不是必须的
  7. switch 后也可以不带表达式,类似 if --else 分支来使用
  8. witch 后也可以直接声明/定义一个变量,分号结束,不推荐
  9. switch 穿透-fallthrough ,如果在 case 语句块后增加 fallthrough ,则会继续执行下一个 case,也叫 switch 穿透
  10. Type Switch:switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际指向的变量类型

循环

go
//
//循环
//@author: Sunjianwang
//
func loop() {
	//第一种写法
	for i := 0; i < 10; i++ {
		fmt.Println("第一种写法:第", i+1, "个")
	}

	//第二种写法
	j := 1
	for j < 10 {
		fmt.Println("第二种写法:第", j+1, "个")
		j++
	}

	//第三种写法
	k := 1
	for {
		if k < 10 {
			fmt.Println("第三种写法:第", k+1, "个")
		} else {
			break
		}
		k++
	}

	//for-range
	str := "hello 中国!"
	for index, val := range str {
		fmt.Printf("第%d个字母:%c\n", index, val)
	}

	//含有中文的字符串使用传统fori遍历,中文会乱码,因为一个中文占三个字节
	//解决方法:将字符串转化成切片
	for i := 0; i < len(str); i++ {
		fmt.Printf("第%d个字母:%c\n", i, str[i])
	}

	//将字符串转化成切片
	strRune := []rune(str)
	for i := 0; i < len(strRune); i++ {
		fmt.Printf("将字符串转化成切片:第%d个字母:%c\n", i, strRune[i])
	}

	//生成随机数,需要使用rand生成一个种子
	//time.Now().Unix(),返回一个从1970年0时到现在的秒数
	//
	var countNum = 0
	var randNomNum = 0
	for {
		rand.Seed(time.Now().UnixMicro())
		randNomNum = rand.Intn(100) + 1
		if randNomNum == 99 {
			break
		} else {
			fmt.Println(randNomNum)
		}
		countNum++
	}
	fmt.Printf("总共运行了%d\n", countNum)

	//break跳出双重循环
label2: //标签,break可以直接跳转到标签
	for i := 0; i < 10; i++ {
		//label:
		for j := 0; j < 10; j++ {
			if j == 2 {
				break label2
			}
			fmt.Printf("i=%v,j=%v\n", i, j)
		}
	}

	fmt.Println("continue练习:")

	//break跳出双重循环
	for i := 0; i < 3; i++ {
		//label:
		for j := 0; j < 5; j++ {
			if j == 2 {
				continue
			}
			fmt.Printf("i=%v,j=%v\n", i, j)
		}
	}
}

goto使用

  1. Go 语言的 goto 语句可以无条件地转移到程序中指定的行。
  2. goto 语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能。
  3. 在 Go 程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难
go
//
//goto练习
//@author: Sunjianwang
//
func gotoTest() {
	//goto跳出双重循环
	for i := 0; i < 3; i++ {
		//label:
		for j := 0; j < 5; j++ {
			if j == 2 {
				goto label
			}
			fmt.Printf("i=%v,j=%v\n", i, j)
		}
	}

label:
	fmt.Println("直接跳转这里执行...")
}

细节使用

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

函数

函数参数传递方式

  1. 值传递:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
  2. 引用传递:指针、slice 切片、map、管道 chan、interface 等都是引用类型

函数使

使用细节

  1. 函数的形参列表可以是多个,返回值列表也可以是多个。支持对函数返回值命名
go
//
//函数实例
//@author: Sunjianwang
//param a
//param b
//@return sum
//@return sub
//
func getSumAndSub(a int, b int) (sum int, sub int) {
	//函数可以返回两个值
	return a + b, a - b
}
  1. 形参列表和返回值列表的数据类型可以是值类型和引用类型。
go
//
//交换a,b
//@author: Sunjianwang
//param a
//param b
//
func swap(a *int, b *int) {
	t := *a
	*a = *b
	*b = t
}
  1. 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似 public,首字母小写,只能被本包文件使用,其它包文件不能使用,类似 private
go
// UtilTest
//工具类测试
//@author: Sunjianwang
//
func UtilTest() {
	fmt.Println("这是一个工具类!")
}
  1. 函数中的变量是局部的,函数外不生效
  2. 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值
  3. 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用
  4. Go 函数不支持函数重载,可以通过可变参数实现类似重载的功能。如果一个函数的形参列表中有可变参数,则可变参数需要放在形参列表最后
go
//
//可变参数在最后
//@author: Sunjianwang
//param a
//param args
//
func argsTest(a string, args ...int) {
	println("形参a = ", a)
	for index, value := range args {
		fmt.Printf("序号%v,参数%v\n", index, value)
	}
}
  1. 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
  2. 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
  3. 为了简化数据类型定义,Go 支持自定义数据类型
go
//自定义数据类型
type myFun func(a int)

//
//函数、自定义类型也可作为形参
//@author: Sunjianwang
//param f
//param a
//
func functionParam(f myFun, a int) {
	f(a)
}
  1. 使用 _ 标识符,忽略返回值
go
//
//函数实例
//@author: Sunjianwang
//param a
//param b
//@return sum
//@return sub
//
func getSumAndSub(a int, b int) (sum int, sub int) {
	//函数可以返回两个值
	return a + b, a - b
}

sum1, _ := getSumAndSub(5, 9) //不接收的返回值可以使用_接收

init函数

每一个源文件都可以包含一个 init 函数,该函数会在 main 函数执行前,被 Go 运行框架调用,也就是说 init 会在 main 函数前被调用

go
//
//每一个源文件都有一个init函数
//@author: Sunjianwang
//
func init() {
	fmt.Println("初始化函数,执行顺序在main之前")
}

使用细节

  1. 如果一个文件同时包含全局变量定义,init 函数和 main 函数,则执行的流程全局变量定义->init 函数->main 函数
  2. init 函数最主要的作用,就是完成一些初始化的工作

匿名函数

Go 支持匿名函数,匿名函数就是没有名字的函数,如果我们某个函数只是希望使用一次,可以考虑使用匿名函数,匿名函数也可以实现多次调用

go
  var (
    //全局匿名函数
    myFunOne = func(a int, b int) int {
      return a - b
    }
  )

	//匿名函数定义方式一
	result := func(a int, b int) int {
		return a + b
	}(1, 2)

	fmt.Println("匿名函数返回值:", result)

	//匿名函数定义方式二
	funA := func(a int, b int) int {
		return a * b
	}
  
  resultA := funA(2, 3)

	fmt.Println("匿名函数A返回值:", resultA)

闭包

闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)

go
// AddUpper
//累加器
//@author: Sunjianwang
//@return func(a int) int
//
func AddUpper() func(a int) int {
	var n int = 10
	var str = "hello"
	return func(x int) int {
		n = n + x
		str += strconv.Itoa(x)
		fmt.Println(str)
		return n
	}
}

func runFunction() {

	f := AddUpper()

	fmt.Println(f(1))	//11
	fmt.Println(f(2))	//13
  fmt.Println(f(3))	//16
}

defer

在函数执行完毕后,及时的释放资源,Go 的设计者提供 defer (延时机制)

go
func add(a int, b int) {
	//当执行到defer语句时,会将defer后面的语句压入defer独立的栈中,暂时不执行
	defer fmt.Println("a= ", a)
	defer fmt.Println("b= ", b)

	fmt.Println("a + b = ", a+b)

	a = 10
	b = 20
}

字符串常用函数

  1. 统计字符串的长度,按字节 len(str)
go
str := "Hello world!"

//func len(v Type) int
//len返回v的长度
//数组:v中元素的数量
//数组指针:*v中元素的数量(v为nil时panic)
//切片、映射:v中元素的数量;若v为nil,len(v)即为零
//字符串:v中字节的数量
//通道:通道缓存中队列(未读取)元素的数量;若v为 nil,len(v)即为零
fmt.Printf("str长度=%v\n", len(str)) //str长度=12
  1. 字符串遍历,同时处理有中文的问题 r := [ ]rune(str)
go
str = "Hello 孙"

fmt.Printf("str带中文长度=%v\n", len(str)) //str带中文长度=9

strRune := []rune(str)

for i := 0; i < len(strRune); i++ {
	fmt.Printf("字符=%c\n", strRune[i])
}

for i, r := range strRune {
	fmt.Printf("位置:%v,值:%c\n", i, r)
}
  1. 字符串转整数;整数转字符串
go
	str = "abc"

	value, err := strconv.Atoi(str)
	if err == nil {
		fmt.Printf("字符串转化整型:%v\n", value)
	} else {
		fmt.Printf("字符串转化整型出错:%v\n", value) //字符串转化整型出错:0
	}

	str = strconv.Itoa(123)
	fmt.Printf("值:%s\n", str)
  1. 字符串 转 [ ]byte
go
str = "Hello 孙"

	byteArr := []byte(str)
	fmt.Printf("byte值:%v\n", byteArr) //byte值:[72 101 108 108 111 32 229 173 153]

	str = string(byteArr)
	fmt.Printf("byte转字符串:%v\n", str) //byte转字符串:Hello 孙
  1. 10 进制转 2, 8, 16 进制
go
	formatInt := strconv.FormatInt(123, 2)
	fmt.Printf("十进制转二进制:%v\n", formatInt) //十进制转二进制:1111011
  1. 查找子串是否在指定的字符串中
go
	contains := strings.Contains("Hello 孙", "孙")
	fmt.Printf("字符串是否包含:%v\n", contains) //字符串包含:true
  1. 统计一个字符串有几个指定的子串
go
	containsNum := strings.Count("Hello 孙", "l")
	fmt.Printf("字符串包含数量:%v\n", containsNum) //字符串包含数量:2
  1. 不区分大小写的字符串比较(== 是区分字母大小写的)
go
	str = "abc"
	str1 := "ABc"
	fmt.Printf("区分大小写比较字符串值:%v\n", str == str1)                   //区分大小写比较字符串值:false
	fmt.Printf("不区分大小写比较字符串值:%v\n", strings.EqualFold(str, str1)) //不区分大小写比较字符串值:true
  1. 返回子串在字符串第一次出现的 index 值,如果没有返回-1
go
	str = "Hello 孙"
	strRune = []rune(str)
	fmt.Printf("第一次出现:%v\n", strings.Index(str, "孙"))             //第一次出现:6
	fmt.Printf("最后一次出现:%v\n", strings.LastIndex(str, "l"))        //最后一次出现:3
	fmt.Printf("取出值:%c\n", strRune[strings.Index(str, "孙")])      //取出值:孙
  //替换指定值:Hello 旺旺,-1表示全部替换
	fmt.Printf("替换指定值:%s\n", strings.Replace(str, "孙", "旺旺", -1))
  1. 按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组
go
	split := strings.Split(str, " ")
	fmt.Printf("字符串数组:%v\n", split) //字符串数组:[Hello 孙]
  1. 将字符串的字母进行大小写的转换
go
	fmt.Printf("字符串转大写:%v\n", strings.ToUpper(str)) //字符串转大写:HELLO 孙
	fmt.Printf("字符串转小写:%v\n", strings.ToLower(str)) //字符串转小写:hello 孙
  1. 将字符串左右两边的空格去掉;判断字符串是否以指定的字符串开头
go
	str = " Hello 孙 "
	fmt.Printf("去掉两边空格:%v\n", strings.TrimSpace(str))             //去掉两边空格:Hello 孙
	fmt.Printf("去掉两边空格:%v\n", strings.Trim(str, " "))             //去掉两边空格:Hello 孙
	fmt.Printf("去掉做边空格:%v\n", strings.TrimLeft(str, " "))         //去掉做边空格:Hello 孙
	fmt.Printf("判断是否以指定字符串为前缀:%v\n", strings.HasPrefix(str, " ")) //判断是否以指定字符串为前缀::true

时间日期常用函数

时间和日期相关函数,需要导入 time 包

  1. 获取日期等信息
go
	curTime := time.Now()
	fmt.Printf("当前时间:%v\n", curTime)
	fmt.Printf("当前年:%v\n", curTime.Year())
	fmt.Printf("当前月:%s\n", curTime.Month()) //默认打印月份英文名称
	fmt.Printf("当前月:%d\n", curTime.Month()) //强转整型,打印月份数字
	fmt.Printf("当前日:%v\n", curTime.Day())
	fmt.Printf("当前时:%v\n", curTime.Hour())
	fmt.Printf("当前分:%v\n", curTime.Minute())
	fmt.Printf("当前秒:%v\n", curTime.Second())
  1. 格式化打印时间
go
	fmt.Printf("格式化时间:%v\n", curTime.Format("2006-01-02 15:01:05"))
	fmt.Printf("格式化时间:%v\n", curTime.Format("2006-01-02"))
  1. 时间的常量
go
const (
	Nanosecond  Duration = 1 //纳秒
	Microsecond          = 1000 * Nanosecond //微秒
	Millisecond          = 1000 * Microsecond //毫秒
	Second               = 1000 * Millisecond //秒
	Minute               = 60 * Second //分钟
	Hour                 = 60 * Minute //小时
)
  1. Sleep
go
	for i := 0; i < 10; i++ {
		println(i + 1)
		//休眠
		time.Sleep(1 * time.Second)
	}
  1. Unix 和 UnixNano
go
func constTime() int64 {

	startTime := time.Now().Unix()

	str := ""

	for i := 0; i < 100000; i++ {
		str += strconv.Itoa(i)
	}

	endTime := time.Now().Unix()

	return endTime - startTime
}

//Unix和UnixNano
fmt.Printf("Unix时间戳:%v,UnixNano时间戳:%v\n", time.Now().Unix(), time.Now().UnixNano())

fmt.Printf("循环100次执行时间:%v\n", constTime())

错误处理

在默认情况下,当发生错误后(panic),程序就会退出(崩溃) Go 中引入的错误处理方式为:defer, panic, recover:Go 中可以抛出一个 panic 的异常,然后在 defer 中通过 recover 捕获这个异常,然后正常处理

错误处理

go
func testError(a int, b int) int {
	//defer,recover进行异常处理
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("发送错误:", err)
		}
	}()
	return a / b
}

自定义错误

go
//
//自定义异常
//@author: Sunjianwang
//param conf
//@return err
//
func readConf(conf string) (err error) {
	if conf == "conf.ini" {
		return nil
	} else {
		//返回自定义错误
		return errors.New("读取文件错误")
	}
}

数组

数组内存布局

总结

  1. 数组的地址可以通过数组名来获取 &intArr
  2. 数组的第一个元素的地址,就是数组的首地址
  3. 数组的各个元素的地址间隔是依据数组的类型决定

数组初始化

go
	//四种初始化数组方式
	var arr1 [3]int = [3]int{1, 2, 3}
	fmt.Printf("arr1:%v\n", arr1)

	var arr2 = [3]int{1, 2, 3}
	fmt.Printf("arr2:%v\n", arr2)

	var arr3 = [...]int{1, 2, 3}
	fmt.Printf("arr3:%v\n", arr3)

	var arr4 = [...]int{0: 1, 1: 99, 4: 20}
	fmt.Printf("arr4:%v\n", arr4)

	var strArr = [...]string{"a", "b", "c"}
	fmt.Printf("strArr:%v\n", strArr)

数组遍历

go
//传统方式遍历
println("传统方式遍历:")
for i := 0; i < len(arr); i++ {
	println(arr[i])
}

//range方式遍历
println("range方式遍历:")
for index, value := range arr {
	fmt.Printf("下标:%v,值:%v\n", index, value)
}

使用细节

  1. 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化
  2. var arr []int,这时 arr 就是一个 slice 切片
  3. 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用
  4. 数组创建后,如果没有赋值,有默认值(零值)
  5. 数组的下标是从 0 开始的
  6. 数组下标必须在指定范围内使用,否则报 panic
  7. Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响
  8. 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
  9. 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度

切片

基本介绍

  1. 切片的英文是 slice
  2. 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
  3. 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
  4. 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
  5. 切片定义的基本语法: var 切片名 []类型

内存布局

切片创建

go
	var arr = [5]int{1, 2, 3, 4, 5}

	//下标为1到3的元素,但是不包含下标为3的元素
	//slice本质上是一个结构体
	//type SliceHeader struct {
	//	Data uintptr  //数据地址
	//	Len  int	//切片元素个数
	//	Cap  int	//切片容量
	//}
	var slice = arr[1:3]

	fmt.Printf("arr[1]的地址:%p\n", &arr[1])
	fmt.Printf("slice[0]的地址:%p\n", &slice[0])

	//引用已经创建好的数组
	var arr = [5]int{1, 2, 3, 4, 5}

	var slice = arr[:]

	fmt.Printf("引用数组的切片的值:%v\n", slice)

	//make创建
	var makeSlice = make([]int, 4, 4)

	makeSlice[2] = 9

	makeSlice = append(makeSlice, 1)

	fmt.Printf("make创建的切片:%v\n", makeSlice)

	//底层引用了一个不可见的数组
	fmt.Printf("make创建的slice执行数组地址:%p\n", &makeSlice[0])

	//直接指定数组
	var thirdSlice = []int{1, 2, 3}

	fmt.Printf("直接创建的切片:%v\n", thirdSlice)
	fmt.Printf("直接创建的切片的容量:%v\n", cap(thirdSlice))
  
  	//新增元素
	//切片是引用类型,如果新增元素后,切片个数没有超过其容量,则新元素会覆盖原来数据的元素
	//如果超过切片容量,则切片进行自动扩容,并将数组进行值拷贝到新数组中(地址进行变化)
	slice = append(slice, 666)
	//
	//新增元素
	slice = append(slice, 777)
	//
	////新增元素
	//slice = append(slice, 888)

	fmt.Println("切片值:", slice)

	fmt.Println("切片元素个数:", len(slice))

	fmt.Println("切片容量:", cap(slice))

	for index, value := range slice {
		slice[index] = value + 1
	}

	for _, value := range arr {
		fmt.Println("遍历arr:", value)
	}

使用细节

go
	var arr = [5]int{1, 2, 3, 4, 5}

	//arr[:4]等价于arr[0:4]
	var slice = arr[:4]

	fmt.Printf("从0开始,切片的值:%v\n", slice) //从0开始,切片的值:[1 2 3 4]

	//arr[1:]等价于arr[1:len(arr)]
	slice = arr[1:]

	fmt.Printf("从一个下标开始,到切片最后一个元素,切片的值:%v\n", slice) //从一个下标开始,到切片最后一个元素,切片的值:[2 3 4 5]

	var newSlice = slice[2:]
	fmt.Printf("切片可以继续切:%v\n", newSlice)                               //切片可以继续切:[4 5]
	fmt.Printf("数组第二个元素地址:%p,第三个元素地址:%p\n", &arr[1], &arr[2])          //数组第二个元素地址:0xc00000a548,第三个元素地址:0xc00000a550
	fmt.Printf("第一个切片指向地址:%p,第一个切片第二个元素地址:%p\n", &slice[0], &slice[1]) //第一个切片指向地址:0xc00000a548,第一个切片第二个元素地址:0xc00000a550
	fmt.Printf("第二个切片指向地址:%p,值:%v\n", &newSlice[0], newSlice[0])       //第二个切片指向地址:0xc00000a558,值:4

	slice = arr[:4]
	fmt.Printf("截取数组全部,切片的值:%v\n", slice) //截取数组全部,切片的值:[1 2 3 4]
	fmt.Printf("切片的切片的变化:%v\n", newSlice) //切片的切片的变化:[4 5]
	//slice = append(slice, 99, 100)
	//
	//fmt.Printf("切片追加元素:%v\n", slice)

	fmt.Println("当前切片容量:", cap(slice))             //当前切片容量: 5
	fmt.Printf("切片长度小于容量时,切片引用地址:%p\n", &slice[0]) //切片长度小于容量时,切片引用地址:0xc00000a540
	slice = append(slice, 99)
	fmt.Printf("追加一个元素,切片引用地址:%p\n", &slice[0]) //追加一个元素,切片引用地址:0xc00000a540
	slice = append(slice, 666)
	fmt.Printf("追加两个元素,切片自动扩容,切片引用地址:%p\n", &slice[0]) //追加两个元素,切片自动扩容,切片引用地址:0xc000014230

	fmt.Printf("切片的切片的变化:%v\n", newSlice) //切片的切片的变化:[4 99]
	fmt.Printf("copy前:%v\n", slice)       //copy前:[1 2 3 4 99 666]
	copy(slice, newSlice)
	fmt.Printf("copy后:%v\n", slice) //copy后:[4 99 3 4 99 666]
  1. 切片初始化时 var slice = arr[startIndex:endIndex]
  2. 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长
  3. cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素:func cap(v Type) int
  4. 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用
  5. 切片可以继续切片
  6. 用 append 内置函数,可以对切片进行动态追加:func append(slice []Type, elems ...Type) []Type
  7. 切片使用 copy 内置函数完成拷贝:func copy(dst, src []Type) int
  8. 切片是引用类型,所以在传递时,遵守引用传递机制

string和slice

  1. string 底层是一个 byte 数组,因此 string 也可以进行切片处理
  2. string 和切片在内存的形式,以 "abcd" 画出内存示意图
  3. string 是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串
  4. 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string
go
//
//string和slice
//@author: Sunjianwang
//
func stringAndSlice() {

	//string底层是byte数组
	str := "Hello 旺"

	sl := str[6:]

	fmt.Printf("sl: %v\n", sl) //sl: 旺

	//string不能通过str[0] = 'x'这种方式进行字符串的修改,可以有如下方式:
	//1. 将string字符串转化为[]byte,再进行字符修改。这种方式不能修改中文。
	//2. 将string字符串转化为[]rune,再进行修改。这种方式可以修改中文。

	byteArr := []byte(str)
	byteArr[0] = 'h'
	fmt.Printf("修改后字符串:%v\n", string(byteArr)) //修改后字符串:hello 旺
	runeArr := []rune(str)
	runeArr[6] = ''
	fmt.Printf("修改后字符串:%v\n", string(runeArr)) //修改后字符串:Hello 孙

	fmt.Printf("验证字符串是否被修改:%v\n", str) //验证字符串是否被修改:Hello 旺
}

二维数组

go
	var array [5][5]int

	rand.Seed(time.Now().UnixNano())

	for i := 0; i < len(array); i++ {
		for j := 0; j < len(array[0]); j++ {
			array[i][j] = rand.Intn(20)
		}
	}
  
  var arr = [...][3]int{{1, 3, 4}, {2, 5, 6}}

	for i, va := range arr {
		for j, va2 := range va {
			fmt.Printf("arr[%v][%v] = %v\n", i, j, va2)
		}
	}
  
  var arr1 = [2][3]int{}
	fmt.Printf("第一种初始化方法:%v\n", arr1)

	var arr2 = [...][3]int{{1, 3, 4}, {2, 5, 6}}
	fmt.Printf("第二种初始化方法:%v\n", arr2)

Map

Map初始化

  1. map 在使用前一定要make
  2. map 的key 是不能重复,如果重复了,则以最后这个key-value 为准
  3. map 的value 是可以相同的.
  4. map 的key-value 是无序
  5. make 内置函数数目
go
//方式一:先声明,后make
var myMap map[string]string

myMap = make(map[string]string, 2)

myMap["name"] = "孙"

//方式二:直接make创建
var myMap1 = make(map[string]string, 5)

myMap1["name"] = "孙"
myMap1["name1"] = "建"
myMap1["name2"] = "旺"

fmt.Printf("myMap1: %v\n", myMap1)
fmt.Println("myMap1的长度:", len(myMap1))

//方式三:直接赋值
var myMap2 = map[string]string{
    "name": "孙",
    "age":  "18",
}

//map[string]map[string][string]
fmt.Printf("myMap2: %v\n", myMap2)

var myMap = make(map[string]map[string]string, 3)

//必须初始化
myMap["student"] = make(map[string]string, 3)
myMap["student"]["name"] = "孙"
myMap["student"]["age"] = "孙"
myMap["student"]["address"] = "孙"

myMap["student1"] = make(map[string]string, 3)
myMap["student1"]["name"] = "孙"
myMap["student1"]["age"] = "孙"
myMap["student1"]["address"] = "孙"

myMap["student2"] = make(map[string]string, 3)
myMap["student2"]["name"] = "孙"
myMap["student2"]["age"] = "孙"
myMap["student2"]["address"] = "孙"

fmt.Printf("myMapMap: %v\n", myMap)

Map使用细节

  1. map 是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的map
  2. map 的容量达到后,再想map 增加元素,会自动扩容,并不会发生panic,也就是说map 能动态的增长键值对(key-value)
  3. map 的value 也经常使用struct 类型,更适合管理复杂的数据(比前面value 是一个map 更好)
go
var myMap = make(map[string]string, 3)

//map[string]interface{},值可以为任意类型
var mapArr []map[string]interface{}

//map[key],如果key不存在就新增,存在就修改
myMap["name"] = "孙"
myMap["age"] = "24"

//内建函数
//如果要删除所有元素:
//1. 遍历map,删除每一个
//2. 直接make一个新map,使原来的成为空的,被GC回收
delete(myMap, "name")

fmt.Printf("myMap的CRUD: %v\n", myMap)

//遍历map
for key, value := range myMap {
    fmt.Printf("key: %v,value: %v\n", key, value)
}

s, boo := myMap["age"]

if boo {
    fmt.Printf("取值:%s\n", s)
} else {
    fmt.Println("没有这个值!")
}

Map切片

go
var sliceMap = make([]map[string]string, 0)

sliceMap = append(sliceMap, map[string]string{
    "name": "孙",
})

sliceMap = append(sliceMap, map[string]string{
    "age": "24",
})

fmt.Printf("sliceMap的值:%v\n", sliceMap)

Struct结构体

结构体定义

go
type student struct {
	name  string
	age   int
	sex   string
	ptr   *int
	slice []int
	myMap map[string]string
}

//
// createStruct
// @Description: 结构体定义
//
func createStruct() {

	//方式一
	var s1 student

	var num = 100

	s1.name = "孙"
	s1.age = 18
	s1.sex = "男"

	s1.ptr = &num

	//slice引用类型,需要先初始化
	s1.slice = make([]int, 5)
	s1.slice[0] = 100

	//map引用类型,需要先初始化
	s1.myMap = make(map[string]string, 5)
	s1.myMap["name"] = "旺"

	//方式二
	s2 := student{age: 18}
	s2.name = "旺"

	//方式三
	var student1 *student = new(student)

	//因为student1是一个指针,所以标准的字段赋值方式为
	(*student1).sex = "标准方式赋值"

	//也可以直接赋值
	//Go都设计者为了使用方便,在底层会对 student1.name = "指针"
	//进行处理,会给student1加上取值运算
	student1.name = "指针"

	var student2 *student = student1

	//引用传递,修改会影响student1
	student2.name = "旺"

	fmt.Printf("student1的值:%v\n", student1) //student1的值:&{旺 0 标准方式赋值 <nil> [] map[]}

	//方式4
	var student4 = &student{}
	student4.name = "孙"

	fmt.Printf("student4都值:%v\n", student4) //student4都值:&{孙 0  <nil> [] map[]}
}

结构体赋值

go
type student struct {
	name  string
	age   int
	sex   string
	ptr   *int
	slice []int
	myMap map[string]string
}

//
// structMemory
// @Description: 结构体赋值
//
func structMemory() {

	var student1 = student{}
	student1.name = "旺"

	var student2 = student1
	student2.name = "孙"

	//值拷贝
	//student1的值:旺,student2的值孙
	fmt.Printf("student1的值:%v,student2的值%v\n", student1.name, student2.name)

	//地址拷贝
	var student3 = &student1
	student3.name = "Mac"

	//student1的值:Mac,student3的值:Mac
	fmt.Printf("student1的值:%v,student3的值:%v\n", student1.name, student3.name)

}

结构体使用细节

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

型)

  1. 结构体进行type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
go
type Rect struct {
	leftUp, rightDown Point
}

type Rect2 struct {
	leftUp, rightDown *Point
}

//
// structDetail
// @Description: 结构体使用细节
//
func structDetail() {

	r1 := Rect{Point{1, 2}, Point{3, 4}}

	//四个int,在内存中是连续分布的
	fmt.Printf("r1.leftUp.x的地址:%p,r1.leftUp.y的地址:%p,r1.rightDown.x的地址:%p,r1.rightDown.y的地址:%p\n",
		&r1.leftUp.x,
		&r1.leftUp.y,
		&r1.rightDown.x,
		&r1.rightDown.y)
	//r1.leftUp.x的地址:014000018020,r1.leftUp.y的地址:0x14000018028,r1.rightDo0x14000018030,r1.rightDown.y的地址:0x14000018038

	r2 := Rect2{&Point{10, 20}, &Point{30, 40}}

	//r2又两个 *Point类型,这两个*Point类型都本身地址也是连续的
	//但是他们指向的地址不一定是连续的
	fmt.Printf("r2.leftUp指向地址:%p,r2.rightDown指向地址:%p\n",
		&r2.leftUp,
		&r2.rightDown)
	//r2.leftUp指向地址:0x140000102b0,r2.rightDown指向地址:0x140000102b8

	type A struct {
		num int
	}

	type B struct {
		num int
	}

	var a A
	var b B

	//可以进行结构体转换,但是结构体都字段要完全一样,包括名字,个数和类型
	b = B(a)

	fmt.Println(b)

}

结构体序列化

  1. struct 的每个字段上,可以写上一个tag, 该tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化
go
type Monster struct {
	//字段名首字母小写的话,字段不会被序列化
	Name  string `json:"name"` //tag:在进行序列化时,使用tag名称;PS:使用反射获取
	Age   int    `json:"age"`
	Skill string `json:"skill"`
}

func structJson() {

	monster := Monster{
		Name:  "旺",
		Age:   24,
		Skill: "内卷",
	}

	marshal, _ := json.Marshal(monster)

	fmt.Printf("序列化后的值:%v", string(marshal))//序列化后的值:{"name":"旺","age":24,"skill":"内卷"}
}

结构体方法

  1. 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式;如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
  2. Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int , float32 等都可以有方法
  3. 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问
  4. 如果一个类型实现了String()这个方法,那么fmt.Println 默认会调用这个变量的String()进行输

go
type Student struct {
	Name string
	Age  int
	sex  string
}

// 方法绑定的结构体形参类型要保持一致:都为值类型或者都为指针类型
func (s *Student) study() {

}

// 结构体Student的方法,与Student绑定,只能通过Student类型变量进行访问
// 类似于Java中一个类的方法(个人理解)
// 方法中的参数,值拷贝还是地址拷贝区别在于形参类型,而不取决于实参类型
func (s *Student) toString() {

	//值类型传参,不会对参数进行修改
	s.Name = "孙"

	fmt.Printf("Name:%v,Age:%v,sex:%v\n", s.Name, s.Age, s.sex)

}

// methodTest
// @Description: 入口函数
func methodTest() {

	s1 := Student{
		Name: "旺",
		Age:  24,
		sex:  "男",
	}

	s1.toString()
	fmt.Printf("s1.Name的值:%v\n", s1.Name)

	s2 := &Student{
		Name: "旺",
		Age:  24,
		sex:  "男",
	}

	s2.toString()
	fmt.Printf("s2.Name的值:%v\n", s2.Name)

}

面向对象编程

封装

go
/**
* 封装练习
*@author: Sunjianwang
*@version: 1.0
 */

package util

import "fmt"

type person struct {
	name  string
	age   int8
	phone string
}

func NewPerson() *person {
	return &person{}
}

func (p *person) SetName(name string) {
	p.name = name
}

func (p *person) ToString() {
	fmt.Printf("name=%v, age=%v, phone=%v\n", p.name, p.age, p.phone)
}

继承

go
/**
* 继承
*@author: Sunjianwang
*@version: 1.0
 */

package main

import "fmt"

// BaseStudent
// @Description: 基础结构体
type BaseStudent struct {
	name  string
	age   int
	score float32
	//可以定义匿名基本数据类型,同种类型有且只能有一个
	int
}

func (base *BaseStudent) learn() {
	fmt.Println("学生会学习")
}

func (base *BaseStudent) test() {
	fmt.Println("学生会考试")
}

// BaseStudent2
// @Description: 基础结构体
type BaseStudent2 struct {
	score float32
}

type SmallStudent struct {
	//通过嵌入匿名结构体的方式进行继承
	BaseStudent
	BaseStudent2
	filed string
}

func (small SmallStudent) cry() {
	fmt.Println("小学生会哭")
}

type BigStudent struct {
	BaseStudent
	SmallStudent
	love string
}

func (big BigStudent) fallInLove() {
	fmt.Println("大学生会谈恋爱")
}

func (big BigStudent) test() {
	fmt.Println("就近原则:优先执行此方法!")
}

func extends() {

	small := SmallStudent{
		filed: "特有属性",
	}

	//共有属性赋值,通过匿名结构体访问公共属性
	small.BaseStudent.name = "小学生的名字"

	//直接访问公共属性
	small.name = "直接访问"

	//如果结构体继承了多个结构体,且结构体间有相同的属性或方法,则必须使用结构体名去访问属性
	small.BaseStudent2.score = 99

	small.cry()
	small.learn()
	fmt.Println("small的name值:", small.BaseStudent.name)

	big := BigStudent{
		BaseStudent{
			name:  "旺",
			age:   0,
			score: 0,
		},
		SmallStudent{
			BaseStudent:  BaseStudent{},
			BaseStudent2: BaseStudent2{},
			filed:        "",
		},
		"测试",
	}

	big.BaseStudent.age = 22

	//直接访问匿名基本数据类型
	big.int = 10

	big.fallInLove()
	big.test()
	fmt.Println("big的age值:", big.BaseStudent.age)
}

多态(接口)

go
/**
* 接口练习
*@author: Sunjianwang
*@version: 1.0
 */

package main

import "fmt"

//
// Device
// @Description: Device接口:实现其所有定义方法的结构体可以视为实现了这个接口
//
type Device interface {
	open()
	close()
	show()
}

type BaseFiled struct {
	deviceType string
	name       string
}

type Mobile struct {
	BaseFiled
}

func (m Mobile) open() {
	fmt.Println(m.name, m.deviceType, "会开机")
}

func (m Mobile) close() {
	fmt.Println(m.name, m.deviceType, "会关机")
}

func (m Mobile) show() {
	fmt.Println(m.name, m.deviceType, "会显示")
}

type Computer struct {
	BaseFiled
}

func (c Computer) open() {
	fmt.Println(c.name, c.deviceType, "会开机")
}

func (c Computer) close() {
	fmt.Println(c.name, c.deviceType, "会关机")
}

func (c Computer) show() {
	fmt.Println(c.name, c.deviceType, "会显示")
}

type BuyDevice struct {
}

func (d BuyDevice) testDevice(device Device) {
	device.open()
	device.close()
	device.show()
}

func interfaceDemo() {

	b := BuyDevice{}

	m := Mobile{BaseFiled{
		deviceType: "手机",
		name:       "IPhone",
	}}

	c := Computer{BaseFiled{
		deviceType: "电脑",
		name:       "Mac Book Pro",
	}}

	b.testDevice(m)

	b.testDevice(c)
}

类型断言

  1. 类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
  2. 在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型
go
//
// testDevice
// @Description: 多态体现
// @receiver d
// @param device 实现device
//
func (d BuyDevice) testDevice(device Device) {
	device.open()
	device.close()
	device.show()
	//断言,判断是否可以强转成这种类型
	mobile, ok := device.(Mobile)
	if ok {
		mobile.call()
	}else {
		fmt.Println("类型转换失败!")
	}
}

文件操作

文件打开方式

go
const (
	// Exactly one of O_RDONLY, O_WRONLY, or O_RDWR must be specified.
	O_RDONLY int = syscall.O_RDONLY // open the file read-only. 以只读方式打开
	O_WRONLY int = syscall.O_WRONLY // open the file write-only. 以写入模式打开
	O_RDWR   int = syscall.O_RDWR   // open the file read-write. 以读写模式打开
	// The remaining values may be or'ed in to control behavior.
	O_APPEND int = syscall.O_APPEND // append data to the file when writing. 写入时将数据追加到文件
	O_CREATE int = syscall.O_CREAT  // create a new file if none exists. 如果文件不存在则创建
	O_EXCL   int = syscall.O_EXCL   // used with O_CREATE, file must not exist. 和O_CREATE一起使用,文件必须不存在
	O_SYNC   int = syscall.O_SYNC   // open for synchronous I/O.打开文件用于同步I/O
	O_TRUNC  int = syscall.O_TRUNC  // truncate regular writable file when opened. 如果可能,打开时清空文件
)

文件读取

go
//
// fileTest
// @Description: 文件读取基本操作
//
func fileTest() {

	//打开一个文件,返回一个file句柄(指针)和一个错误(如果成功打开err为nil)
	file, err := os.Open("/Users/sunjianwang/Downloads/PMCC_DN_POWER_OUTAGES_GZ.sql")

	//记得释放
	defer file.Close()
	if file != nil {
		//使用reader读取文件内容
		reader := bufio.NewReader(file)
		
		//循环读取,知道返回io.EOF错误
		for {
			readString, err := reader.ReadString('\n')
			fmt.Println(readString)
			if err == io.EOF {
				break
			}
		}
		
		//直接打开并读取文件,返回一个byte切片及错误
		readFile, err := os.ReadFile("/Users/sunjianwang/Downloads/PMCC_DN_POWER_OUTAGES_GZ.sql")
		//打印文件内容
		fmt.Println(string(readFile))
		if err != nil {
			fmt.Println(err)
		}
	} else {
		fmt.Println(err)
	}

}

文件创建及写入

go
// createFile
// @Description: 创建一个文件,并写入内容
func createFileIfNoFile() {
  //如果文件不存在,则创建
	file, err := os.OpenFile("/Users/sunjianwang/Downloads/test.txt", os.O_WRONLY|os.O_CREATE, os.ModePerm)
	if err != nil {
		fmt.Println("打开/创建文件失败", err)
	}
	writer := bufio.NewWriter(file)

	writer.WriteString("Hello World!!!")

	//刷新缓存,将内容写入到文件中
	writer.Flush()

	defer file.Close()
}

文件内容追加

go
// openFileAndAppend
// @Description: 打开一个文件,并追加内容
func openFileAndAppend() {
	file, err := os.OpenFile("/Users/sunjianwang/Downloads/test.txt", os.O_APPEND|os.O_WRONLY, os.ModePerm)

	if err != nil {
		fmt.Println("不存在此文件!", err)
	}

	defer file.Close()

	writer := bufio.NewWriter(file)
	writer.WriteString("这是新增加的内容!!")
	writer.Flush()
}

文件内容覆盖

go
// openFileAndWrite
// @Description: 打开一个已存在的文件,并覆盖文件内容
func openFileAndWrite() {

	file, err := os.OpenFile("/Users/sunjianwang/Downloads/test.txt", os.O_TRUNC|os.O_WRONLY, os.ModePerm)

	if err != nil {
		fmt.Println("不存在此文件!", err)
	}

	defer file.Close()

	writer := bufio.NewWriter(file)

	writer.WriteString("加油啊,旺仔!")

	writer.Flush()
}

文件内容拷贝

go
// copyFileContent
// @Description: 将一个文件内容拷贝到另一个文件中
func copyFileContent() {

	file, err := os.ReadFile("/Users/sunjianwang/Downloads/test.txt")
	if err != nil {
		fmt.Println("源文件不存在")
	}

	errTar := os.WriteFile("/Users/sunjianwang/Downloads/test1.txt", file, os.ModePerm)

	if errTar == nil {
		fmt.Println("写入文件成功:")
	}
}

判断文件是否存在

该函数返回文件信息,可通过返回的err信息来判断文件是否存在

go
// fileInfo
// @Description: 返回文件信息,可用来判断文件或文件夹是否存在
func fileInfo() {

	stat, err := os.Stat("/Users/sunjianwang/Downloads")

	if err != nil {
		fmt.Println("文件或文件夹不存在")
	}

	fmt.Println(stat.Name())
}

序列化

在线JSON解析

Json序列化

go
type StudentForJson struct {
	Name     string            `json:"name"`
	Age      int8              `json:"age"`
	Grade    int8              `json:"grade"`
	Email    string            `json:"email"`
	ValueMap map[string]string `json:"valueMap"`
}

func jsonTest() {

  //要序列化的结构体
	stu := StudentForJson{
		Name:  "旺仔",
		Age:   24,
		Grade: 99,
		Email: "15133307201@163.com",
		ValueMap: map[string]string{
			"语文": "98",
			"数学": "99",
		},
	}

	marshal, err := json.Marshal(stu)
	if err != nil {
		fmt.Println("序列化失败")
	}

	log.Printf("序列化的值:%v\n", string(marshal))

  //要序列化的map切片
	var mapArr []map[string]interface{}

	mapArr = append(mapArr, map[string]interface{}{
		"name": "旺仔",
		"age":  "24",
	})

	mapArr = append(mapArr, map[string]interface{}{
		"name": "阳仔",
		"age":  "24",
	})

	mapArrByte, _ := json.Marshal(mapArr)
	fmt.Printf("map切片:%v\n", string(mapArrByte))
}

Json反序列化

go
type StudentForJson struct {
	Name     string            `json:"name"`
	Age      int8              `json:"age"`
	Grade    int8              `json:"grade"`
	Email    string            `json:"email"`
	ValueMap map[string]string `json:"valueMap"`
}

func jsonTest() {

	stu := StudentForJson{
		Name:  "旺仔",
		Age:   24,
		Grade: 99,
		Email: "15133307201@163.com",
		ValueMap: map[string]string{
			"语文": "98",
			"数学": "99",
		},
	}

	marshal, err := json.Marshal(stu)
	if err != nil {
		fmt.Println("序列化失败")
	}

	log.Printf("序列化的值:%v\n", string(marshal))

	var newStu = &StudentForJson{}

  //Unmarshal进行反序列化,需传入指针类型
	err = json.Unmarshal(marshal, newStu)
  
	if err == nil {
		fmt.Printf("姓名:%v\n", newStu.ValueMap)
	} else {
		fmt.Println("反序列化失败:", err)
	}
}

单元测试

使用总结

  1. 测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go , cal 不是固定的。

  2. 测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper

  3. TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T 【看一下手册】

  4. 一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub

  5. 运行测试用例指令

    • cmd>go test [如果运行正确,无日志,错误时,会输出日志]
    • cmd>go test -v [运行正确或是错误,都输出日志]
  6. 当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序

  7. t.Logf 方法可以输出相应的日志

  8. 测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处[原理图].

  9. PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败

  10. 测试单个文件,一定要带上被测试的原文件go test -v cal_test.go cal.go

  11. 测试单个方法go test -v -test.run TestAddUpper

go
/**
* 被测试代码
*@author: Sunjianwang
*@version: 1.0
 */

package testCase

func addUpper(n int) int {
	var num = 0
	for i := 1; i <= n; i++ {
		num += i
	}
	return num
}
go
/**
* 单元测试文件
*@author: Sunjianwang
*@version: 1.0
 */

package testCase

import (
	"testing"
)

func TestAddUpper(t *testing.T) {

	//调用
	num := addUpper(10)

	if num != 55 {
		//调用 Fatalf 相当于在调用 Logf 之后调用 FailNow
		t.Fatalf("程序错误,执行结果:%v", num)
	}

	t.Log("执行成功")
}