Golang 官方网站 https://golang.org Golang 中文网 在线标准库文档: https://studygolang.com/pkgdoc
变量使用
声明变量
// 定义全局变量
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将字符串算到基本数据类型中
基本数据类型使用
整数类型
类型 | 有无符号 | 占用存储空间 | 表数范围 | 备注 |
---|---|---|---|---|
int | 有 | 32位系统4个字节 64位系统8个字节 | -231 ~ 231-1 -263 ~ 263-1 | |
int8 | 有 | 1字节 | -128 ~ 127 | |
int16 | 有 | 2字节 | -215 ~ 215-1 | |
int32 | 有 | 4字节 | -231 ~ 231-1 | |
int64 | 有 | 8字节 | -263 ~ 263-1 | |
uint | 无 | 32位系统4个字节 64位系统8个字节 | 0 ~ 232-1 0 ~ 264-1 | |
uint8 | 无 | 1字节 | 0 ~ 255 | |
uint16 | 无 | 2字节 | 0 ~ 216-1 | |
uint32 | 无 | 4字节 | 0 ~ 232-1 | |
uint64 | 无 | 8字节 | 0 ~ 264-1 | |
rune | 有 | 与int32一样 | -231 ~ 231-1 | 等价int32,表示一个Unicode码 |
byte | 无 | 与uint8一样 | 0 ~ 255 | 当要存储字符时,选用byte |
/**
*@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))
}
浮点类型
类型 | 占用存储空间 | 表数范围 |
---|---|---|
单精度float32 | 4字节 | -3.403E38 ~ 3.403E38 |
双精度float64 | 8字节 | -1.798E308 ~ 1.798E308 |
说明:
- 关于浮点数在机器中存放形式的简单说明:浮点数=符号位+指数位+尾数位。说明浮点数都是有符号的
- 尾数部分可能丢失,造成精度损失。
使用细节:
- Golang浮点类型有固定的范围和字段长度,不受OS的影响
- Golang的浮点型默认声明为float64类型
- 浮点型常量有两种表示形式: 十进制数形式,如:5.12 科学计数法形式,如:5.1234e2 = 5.1234 * 10^2
- 通常情况下应使用float64,因为它更精准
//
//基本数据类型:浮点型
//@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来保存
说明:
- 如果保存的字符在ASCII表中,可以使用byte保存
- 如果保存的字符码值大于255,可以考虑用int来保存
- 可以使用格式化输出(%c)打印保存的字符
使用细节:
- 字符常量是用单引号('')括起来的单个字符。例如:var c1 byte = 'a' var c2 int = '中' var c3 byte = '9'
- Go中允许使用转义字符'\’来将其后的字符转变为特殊字符型常量。例如:var c3 char = ‘\n’ // '\n'表示换行符
- Go语言的字符使用UTF-8编码,英文字母1个字节 汉字3个字节
- 在Go中,字符的本质是一个整数,直接输出是该字符对应的UTF-8编码的码值。
- 可以直接给某个变量赋一个数字,然后按格式化输出时%c,会输出该数字对应的unicode字符
- 字符类型是可以进行运算的,相当于一个整数,因为它都对应有Unicode码
//
//基本数据类型:字符型
//@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)
}
布尔类型
- 布尔类型也称bool类型,bool类型只允许取值true和false
- bool类型占用一个字节
- bool 类型适于逻辑运算,一般用于程序流程控制
//
//基本数据类型:布尔型
//@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文本
细节
Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本,这样 Golang 统一使用 UTF-8 编码,中文乱码问题不会再困扰程序员
字符串一旦赋值了,字符串就不能修改了:在 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类型
//
//基本数据类型转换: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
}
细节
- Go 中,数据类型的转换可以是从 表示范围小-->表示范围大,也可以 范围大--->范围小
- 被转换的是变量存储的数据(即值),变量本身的数据类型并没有变化
- 在转换中,比如将 int64 转成 int8 【-128---127】 ,编译时不会报错,只是转换的结果是按溢出处理,和我们希望的结果不一样。在转换时,需要考虑范围
注意
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
//
//基本数据类型转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)
}
指针
基本使用
//
//指针基本使用
//@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 等都是引用类型
值类型和引用类型区别
- 值类型:变量直接存储值,内存通常在栈中分配
- 引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收
标识符
标识符概念
- Golang 对各种变量、方法、函数等命名时使用的字符序列称为标识符
- 凡是自己可以起名字的地方都叫标识符
标识符命名规则
- 由 26 个英文字母大小写,0-9 ,_ 组成
- 数字不可以开头。
- Golang 中严格区分大小写。
//Golang中严格区分大小写
var num int = 10
var Num int = 20
fmt.Printf("Golang中严格区分大小写:num=%v,Num=%v", num, Num)
//Golang中严格区分大小写:num=10,Num=20
- 标识符不能包含空格
- 下划线"_"本身在Go中是一个特殊的标识符,称为空标识符。可以代表任何其它的标识符,但是它对应的值会被忽略(比如:忽略某个返回值)。所以仅能被作为占位符使用,不能作为标识符使用
- 不能以系统保留关键字作为标识符(一共有 25 个),比如 break,if 等等
标识符命名规范
- 包名:保持package的名字和目录保持一致,尽量采取有意义的包名,简短,有意义,不要和标准库不要冲突
- 变量名、函数名、常量名:采用驼峰法
- 如果变量名、函数名、常量名首字母大写,则可以被其他的包访问;如果首字母小写,则只能在本包中使用 ( 注:可以简单的理解成,首字母大写是公开的,首字母小写是私有的) ,在 golang 没有public , private 等关键字
运算符
算数运算符
取整、取模、自增自减
//
//取整、取模、自增自减
//@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)
}
使用细节
- 对于除号 "/",它的整数除和小数除是有区别的:整数之间做除法时,只保留整数部分而舍弃小数部分。 例如: x := 19/5 ,结果是3
- 当对一个数取模时,可以等价 a%b=a-a/b*b , 这样我们可以看到 取模的一个本质运算。
- Golang 的自增自减只能当做一个独立语言使用时,不能这样使用
- Golang 的++ 和 -- 只能写在变量的后面,不能写在变量的前面,即:只有 a++ a-- 没有 ++a --a
关系运算符
运算符 | 运算 |
---|---|
== | 等于 |
!= | 不等于 |
> | 大于 |
< | 小于 |
>= | 大于等于 |
<= | 小于等于 |
逻辑运算符
运算符 | 描述 |
---|---|
&& | 逻辑与(短路与):全真,结果为true,否则为false(如果第一个条件为false,则后面的条件不会进行判断,直接返回false) |
|| | 逻辑或(短路或):有一个为真则结果为true,全假则结果为false(如果第一个条件为true,则后面的条件不会进行判断,直接返回false) |
! | 逻辑非:条件为true,则逻辑非为false |
赋值运算符
运算符 | 描述 |
---|---|
= | 简单的赋值运算符 |
+= | 相加后赋值 |
-= | 相减后赋值 |
*= | 相乘后赋值 |
/= | 整除后赋值 |
%= | 求余后赋值 |
<<= | 左移后赋值 |
>>= | 右移后赋值 |
&= | 按位与,然后复制 |
^= | 按位异或,然后赋值 |
!= | 按位或,然后赋值 |
//不使用中间变量,将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)
运算符执行顺序
大体顺序
- 括号,++, --
- 单目运算
- 算术运算符
- 移位运算
- 关系运算符
- 位运算符
- 逻辑运算符
- 赋值运算符
- 逗号
流程控制
分支
//
//流程控制
//@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)
}
}
使用细节
- case/switch 后是一个表达式( 即:常量值、变量、一个有返回值的函数等都可以)
- case 后的各个表达式的值的数据类型,必须和 switch 的表达式数据类型一致
- case 后面可以带多个表达式,使用逗号间隔。比如 case 表达式 1,表达式 2...
- case 后面的表达式如果是常量值(字面量),则要求不能重复
- case 后面不需要带break , 程序匹配到一个 case 后就会执行对应的代码块,然后退出 switch,如果一个都匹配不到,则执行 default
- default 语句不是必须的
- switch 后也可以不带表达式,类似 if --else 分支来使用
- witch 后也可以直接声明/定义一个变量,分号结束,不推荐
- switch 穿透-fallthrough ,如果在 case 语句块后增加 fallthrough ,则会继续执行下一个 case,也叫 switch 穿透
- Type Switch:switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际指向的变量类型
循环
//
//循环
//@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使用
- Go 语言的 goto 语句可以无条件地转移到程序中指定的行。
- goto 语句通常与条件语句配合使用。可用来实现条件转移,跳出循环体等功能。
- 在 Go 程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难
//
//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("直接跳转这里执行...")
}
包
细节使用
- 在给一个文件打包时,该包对应一个文件夹,比如这里的 utils 文件夹对应的包名就是 utils,文件的包名通常和文件所在的文件夹名一致,一般为小写字母
- 当一个文件要使用其它包函数或变量时,需要先引入对应的包
- 引入方式 1:
import "包名"
- 引入方式 2:
- 引入方式 1:
import (
"包名"
"包名"
)
- package 指令在 文件第一行,然后是 import 指令
- 在 import 包时,路径从 $GOPATH 的 src 下开始,不用带 src , 编译器会自动从 src 下开始引入
- 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母需要大写,类似其它语言的 public ,这样才能跨包访问
- 在访问其它包函数,变量时,其语法是 包名.函数名, 比如这里的 main.go 文件中
- 如果包名较长,Go支持给包取别名, 注意细节:取别名后,原来的包名就不能使用了
- 在同一包下,不能有相同的函数名(也不能有相同的全局变量名),否则报重复定义
- 如果你要编译成一个可执行程序文件,就需要将这个包声明为 main , 即 package main .这个就是一个语法规范,如果你是写一个库 ,包名可以自定义
函数
函数参数传递方式
- 值传递:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct
- 引用传递:指针、slice 切片、map、管道 chan、interface 等都是引用类型
函数使
使用细节
- 函数的形参列表可以是多个,返回值列表也可以是多个。支持对函数返回值命名
//
//函数实例
//@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
}
- 形参列表和返回值列表的数据类型可以是值类型和引用类型。
//
//交换a,b
//@author: Sunjianwang
//param a
//param b
//
func swap(a *int, b *int) {
t := *a
*a = *b
*b = t
}
- 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似 public,首字母小写,只能被本包文件使用,其它包文件不能使用,类似 private
// UtilTest
//工具类测试
//@author: Sunjianwang
//
func UtilTest() {
fmt.Println("这是一个工具类!")
}
- 函数中的变量是局部的,函数外不生效
- 基本数据类型和数组默认都是值传递的,即进行值拷贝。在函数内修改,不会影响到原来的值
- 如果希望函数内的变量能修改函数外的变量(指的是默认以值传递的方式的数据类型),可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用
- 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)
}
}
- 在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用
- 函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用
- 为了简化数据类型定义,Go 支持自定义数据类型
//自定义数据类型
type myFun func(a int)
//
//函数、自定义类型也可作为形参
//@author: Sunjianwang
//param f
//param a
//
func functionParam(f myFun, a int) {
f(a)
}
- 使用 _ 标识符,忽略返回值
//
//函数实例
//@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 函数前被调用
//
//每一个源文件都有一个init函数
//@author: Sunjianwang
//
func init() {
fmt.Println("初始化函数,执行顺序在main之前")
}
使用细节
- 如果一个文件同时包含全局变量定义,init 函数和 main 函数,则执行的流程全局变量定义->init 函数->main 函数
- init 函数最主要的作用,就是完成一些初始化的工作
匿名函数
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)
闭包
闭包就是一个函数和与其相关的引用环境组合的一个整体(实体)
// 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 (延时机制)
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
}
字符串常用函数
- 统计字符串的长度,按字节 len(str)
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
- 字符串遍历,同时处理有中文的问题 r := [ ]rune(str)
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)
}
- 字符串转整数;整数转字符串
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)
- 字符串 转 [ ]byte
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 孙
- 10 进制转 2, 8, 16 进制
formatInt := strconv.FormatInt(123, 2)
fmt.Printf("十进制转二进制:%v\n", formatInt) //十进制转二进制:1111011
- 查找子串是否在指定的字符串中
contains := strings.Contains("Hello 孙", "孙")
fmt.Printf("字符串是否包含:%v\n", contains) //字符串包含:true
- 统计一个字符串有几个指定的子串
containsNum := strings.Count("Hello 孙", "l")
fmt.Printf("字符串包含数量:%v\n", containsNum) //字符串包含数量:2
- 不区分大小写的字符串比较(== 是区分字母大小写的)
str = "abc"
str1 := "ABc"
fmt.Printf("区分大小写比较字符串值:%v\n", str == str1) //区分大小写比较字符串值:false
fmt.Printf("不区分大小写比较字符串值:%v\n", strings.EqualFold(str, str1)) //不区分大小写比较字符串值:true
- 返回子串在字符串第一次出现的 index 值,如果没有返回-1
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))
- 按照指定的某个字符,为分割标识,将一个字符串拆分成字符串数组
split := strings.Split(str, " ")
fmt.Printf("字符串数组:%v\n", split) //字符串数组:[Hello 孙]
- 将字符串的字母进行大小写的转换
fmt.Printf("字符串转大写:%v\n", strings.ToUpper(str)) //字符串转大写:HELLO 孙
fmt.Printf("字符串转小写:%v\n", strings.ToLower(str)) //字符串转小写:hello 孙
- 将字符串左右两边的空格去掉;判断字符串是否以指定的字符串开头
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 包
- 获取日期等信息
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())
- 格式化打印时间
fmt.Printf("格式化时间:%v\n", curTime.Format("2006-01-02 15:01:05"))
fmt.Printf("格式化时间:%v\n", curTime.Format("2006-01-02"))
- 时间的常量
const (
Nanosecond Duration = 1 //纳秒
Microsecond = 1000 * Nanosecond //微秒
Millisecond = 1000 * Microsecond //毫秒
Second = 1000 * Millisecond //秒
Minute = 60 * Second //分钟
Hour = 60 * Minute //小时
)
- Sleep
for i := 0; i < 10; i++ {
println(i + 1)
//休眠
time.Sleep(1 * time.Second)
}
- Unix 和 UnixNano
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 捕获这个异常,然后正常处理
错误处理
func testError(a int, b int) int {
//defer,recover进行异常处理
defer func() {
if err := recover(); err != nil {
fmt.Println("发送错误:", err)
}
}()
return a / b
}
自定义错误
//
//自定义异常
//@author: Sunjianwang
//param conf
//@return err
//
func readConf(conf string) (err error) {
if conf == "conf.ini" {
return nil
} else {
//返回自定义错误
return errors.New("读取文件错误")
}
}
数组
数组内存布局
总结
- 数组的地址可以通过数组名来获取 &intArr
- 数组的第一个元素的地址,就是数组的首地址
- 数组的各个元素的地址间隔是依据数组的类型决定
数组初始化
//四种初始化数组方式
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)
数组遍历
//传统方式遍历
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)
}
使用细节
- 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的, 不能动态变化
- var arr []int,这时 arr 就是一个 slice 切片
- 数组中的元素可以是任何数据类型,包括值类型和引用类型,但是不能混用
- 数组创建后,如果没有赋值,有默认值(零值)
- 数组的下标是从 0 开始的
- 数组下标必须在指定范围内使用,否则报 panic
- Go 的数组属值类型, 在默认情况下是值传递, 因此会进行值拷贝。数组间不会相互影响
- 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
- 长度是数组类型的一部分,在传递函数参数时 需要考虑数组的长度
切片
基本介绍
- 切片的英文是 slice
- 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制。
- 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度 len(slice)都一样。
- 切片的长度是可以变化的,因此切片是一个可以动态变化数组。
- 切片定义的基本语法: var 切片名 []类型
内存布局
切片创建
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)
}
使用细节
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]
- 切片初始化时 var slice = arr[startIndex:endIndex]
- 切片初始化时,仍然不能越界。范围在 [0-len(arr)] 之间,但是可以动态增长
- cap 是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素:
func cap(v Type) int
- 切片定义完后,还不能使用,因为本身是一个空的,需要让其引用到一个数组,或者 make 一个空间供切片来使用
- 切片可以继续切片
- 用 append 内置函数,可以对切片进行动态追加:
func append(slice []Type, elems ...Type) []Type
- 切片使用 copy 内置函数完成拷贝:
func copy(dst, src []Type) int
- 切片是引用类型,所以在传递时,遵守引用传递机制
string和slice
- string 底层是一个 byte 数组,因此 string 也可以进行切片处理
- string 和切片在内存的形式,以 "abcd" 画出内存示意图
- string 是不可变的,也就说不能通过 str[0] = 'z' 方式来修改字符串
- 如果需要修改字符串,可以先将 string -> []byte / 或者 []rune -> 修改 -> 重写转成 string
//
//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 旺
}
二维数组
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初始化
- map 在使用前一定要make
- map 的key 是不能重复,如果重复了,则以最后这个key-value 为准
- map 的value 是可以相同的.
- map 的key-value 是无序
- make 内置函数数目
//方式一:先声明,后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使用细节
- map 是引用类型,遵守引用类型传递的机制,在一个函数接收map,修改后,会直接修改原来的map
- map 的容量达到后,再想map 增加元素,会自动扩容,并不会发生panic,也就是说map 能动态的增长键值对(key-value)
- map 的value 也经常使用struct 类型,更适合管理复杂的数据(比前面value 是一个map 更好)
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切片
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结构体
结构体定义
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[]}
}
结构体赋值
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)
}
结构体使用细节
- 结构体是用户单独定义的类型,和其它类型进行转换时需要有完全相同的字段(名字、个数和类
型)
- 结构体进行type 重新定义(相当于取别名),Golang 认为是新的数据类型,但是相互间可以强转
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)
}
结构体序列化
- struct 的每个字段上,可以写上一个tag, 该tag 可以通过反射机制获取,常见的使用场景就是序列化和反序列化
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":"内卷"}
}
结构体方法
- 结构体类型是值类型,在方法调用中,遵守值类型的传递机制,是值拷贝传递方式;如程序员希望在方法中,修改结构体变量的值,可以通过结构体指针的方式来处理
- Golang 中的方法作用在指定的数据类型上的(即:和指定的数据类型绑定),因此自定义类型,都可以有方法,而不仅仅是struct,比如int , float32 等都可以有方法
- 方法的访问范围控制的规则,和函数一样。方法名首字母小写,只能在本包访问,方法首字母大写,可以在本包和其它包访问
- 如果一个类型实现了String()这个方法,那么fmt.Println 默认会调用这个变量的String()进行输
出
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)
}
面向对象编程
封装
/**
* 封装练习
*@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)
}
继承
/**
* 继承
*@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)
}
多态(接口)
/**
* 接口练习
*@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)
}
类型断言
- 类型断言,由于接口是一般类型,不知道具体类型,如果要转成具体类型,就需要使用类型断言
- 在进行类型断言时,如果类型不匹配,就会报 panic, 因此进行类型断言时,要确保原来的空接口指向的就是断言的类型
//
// 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("类型转换失败!")
}
}
文件操作
文件打开方式
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. 如果可能,打开时清空文件
)
文件读取
//
// 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)
}
}
文件创建及写入
// 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()
}
文件内容追加
// 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()
}
文件内容覆盖
// 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()
}
文件内容拷贝
// 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信息来判断文件是否存在
// fileInfo
// @Description: 返回文件信息,可用来判断文件或文件夹是否存在
func fileInfo() {
stat, err := os.Stat("/Users/sunjianwang/Downloads")
if err != nil {
fmt.Println("文件或文件夹不存在")
}
fmt.Println(stat.Name())
}
序列化
Json序列化
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反序列化
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)
}
}
单元测试
使用总结
测试用例文件名必须以 _test.go 结尾。 比如 cal_test.go , cal 不是固定的。
测试用例函数必须以 Test 开头,一般来说就是 Test+被测试的函数名,比如 TestAddUpper
TestAddUpper(t *tesing.T) 的形参类型必须是 *testing.T 【看一下手册】
一个测试用例文件中,可以有多个测试用例函数,比如 TestAddUpper、TestSub
运行测试用例指令
- cmd>go test [如果运行正确,无日志,错误时,会输出日志]
- cmd>go test -v [运行正确或是错误,都输出日志]
当出现错误时,可以使用 t.Fatalf 来格式化输出错误信息,并退出程序
t.Logf 方法可以输出相应的日志
测试用例函数,并没有放在 main 函数中,也执行了,这就是测试用例的方便之处[原理图].
PASS 表示测试用例运行成功,FAIL 表示测试用例运行失败
测试单个文件,一定要带上被测试的原文件
go test -v cal_test.go cal.go
测试单个方法
go test -v -test.run TestAddUpper
/**
* 被测试代码
*@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
}
/**
* 单元测试文件
*@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("执行成功")
}