网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
Go语言变量
- Go语言中变量的概念和C语言中也一样, 所以我们直接来看下如何定义和使用变量即可
- C语言中定义变量的格式
数据类型 变量名称;
数据类型 变量名称1, 变量名称2;
#include <stdio.h>
int main(int argc, const char \* argv[])
{
int num1; // 先定义
num1 = 10; // 后初始化
printf("num1 = %d\n", num1);
int num2 = 20; // 定义的同时初始化
printf("num2 = %d\n", num2);
// 注意: 同时定义多个变量,不支持定义时初始化, 只能先定义后初始化
int num3, num4; //同时定义多个变量
num3 = 30;
num4 = 40;
printf("num3 = %d\n", num3);
printf("num4 = %d\n", num4);
return 0;
}
- Go语言中定义变量有三种格式
// 标准格式
var 变量名称 数据类型 = 值;
// 自动推到类型格式
var 变量名称 = 值;
// 简短格式(golang官方推荐格式)
变量名称 := 值;
package main
import "fmt"
func main() {
var num1 int // 先定义
num1 = 10 // 后赋值
fmt.Println("num1 = ", num1)
var num2 int = 20 // 定义的同时赋值
fmt.Println("num2 = ", num2)
var num3 = 30 // 定义的同时赋值, 并省略数据类型
fmt.Println("num3 = ", num3)
num4 := 40 // 定义的同时赋值, 并省略关键字和数据类型
/\*
num4 := 40 等价于
var num4 int
num4 = 40
\*/
fmt.Println("num4 = ", num4)
}
- 和C语言一样,除了可以定义单个变量以外,还支持一次性定义多个变量
- 方式一, 连续定义
package main
import "fmt"
func main() {
var num1, num2 int // 先定义
num1 = 10 // 后赋值
num2 = 20
fmt.Println("num1 = ", num1)
fmt.Println("num2 = ", num2)
var num3, num4 int = 30, 40 // 定义的同时赋值
fmt.Println("num3 = ", num3)
fmt.Println("num4 = ", num4)
var num5, num6 = 50, 60 // 定义的同时赋值, 并省略数据类型
fmt.Println("num5 = ", num5)
fmt.Println("num6 = ", num6)
num7, num8 := 70, 80 // 定义的同时赋值, 并省略关键字和数据类型
fmt.Println("num7 = ", num7)
fmt.Println("num8 = ", num8)
}
+ 方式二, 变量组
package main
import "fmt"
func main() {
var( // 先定义
num1 int
num2 float32
)
num1 = 10 // 后赋值
num2 = 3.14
fmt.Println("num1 = ", num1)
fmt.Println("num2 = ", num2)
var( // 定义的同时赋值
num3 int = 30
num4 float32 = 6.66
)
fmt.Println("num3 = ", num3)
fmt.Println("num4 = ", num4)
var( // 定义的同时赋值, 并省略数据类型
num5 = 50
num6 = 7.77
)
fmt.Println("num5 = ", num5)
fmt.Println("num6 = ", num6)
var( // 一行定义多个
num7, num8 = 70, 80
num9, num10 = 9.99, 100
)
fmt.Println("num7 = ", num7)
fmt.Println("num8 = ", num8)
fmt.Println("num9 = ", num9)
fmt.Println("num10 = ", num10)
}
Go语言变量定义注意点
- 简短模式的含义是定义的同时初始化
package main
import "fmt"
func main() {
num := 10
num := 20 // 编译报错, 重复定义
fmt.Println("num = ", num)
}
- 一定不要把:=当做赋值运算符来使用
package main
import "fmt"
var num = 10 // 定义一个全局变量
func main() {
num := 20 // 定义一个局部变量
fmt.Println("num = ", num)
test()
}
func test() {
fmt.Println("num = ", num) // 还是输出10
}
- :=只能用于定义局部变量,不能用于定义全局变量
package main
import "fmt"
num := 10 // 编译报错
func main() {
fmt.Println("num = ", num)
}
- 使用:=定义变量时,不能指定var关键字和数据类型
package main
import "fmt"
func main() {
//var num int := 10 // 编译报错
//var num := 10 // 编译报错
num int := 10 // 编译报错
fmt.Println("num = ", num)
fmt.Println("num = ", num)
}
- 变量组中不能够使用:=
package main
import "fmt"
func main() {
var(
num := 10 // 编译报错
)
fmt.Println("num = ", num)
}
- 通过:=同时定义多个变量, 必须给所有变量初始化
package main
import "fmt"
func main() {
//num1, num2 := 666, 888 // 正确
num1, num2 := 666 // 报错
fmt.Printf("%d, %d\n", num1, num2)
}
- 通过:=同时定义多个变量, 只要任意一个变量没有定义过,都会做退化赋值操作
package main
import "fmt"
func main() {
// 定义一个变量num1
num1 := 10
// 同时定义两个变量num1和num2, 由于num2从来没有定义过,
// 所以对于num1来说:=退化为赋值运算符, 而对于num2来说:=仍然是定义+赋值
num1, num2 := 20, 30
fmt.Println("num1 = ", num1)
fmt.Println("num2 = ", num2)
}
package main
import "fmt"
func main() {
num1 := 10
num2 := 20
// 报错, 因为num1,和num2都已经被定义过
// 至少要有任意一个变量没有被定义过,才会退化赋值
num1, num2 := 30, 40
fmt.Println("num1 = ", num1)
fmt.Println("num2 = ", num2)
}
- 定义的局部变量或者导入的包没有被使用, 那么编译器会报错,无法编译运行,但是定义的全局变量没有被使用,编译器不会报错, 可以编译运行
局部变量和全局变量
-
和C语言一样,按照变量的作用域,我们可以把变量划分为局部变量和全局变量
-
Go语言中局部变量的概念以及全局变量的概念和C语言一模一样
-
局部变量:
- 定义在函数内部的变量以及函数的形参称为局部变量
- 作用域:从定义哪一行开始直到与其所在的代码块结束
- 生命周期:从程序运行到定义哪一行开始分配存储空间到程序离开该变量所在的作用域
-
全局变量:
- 定义在函数外面的变量称为全局变量
- 作用域范围:从定义哪行开始直到文件结尾
- 生命周期:程序一启动就会分配存储空间,直到程序结束
-
和C语言不同的是, C语言中可以定义相同名称的全局变量, 而Go语言中无论全局变量还是局部变量, 只要作用域相同都不能出现同名的变量
package main
import "fmt"
//var num1 int
//var num1 int // 报错, 重复定义
var num3 int
func main() {
//var num2
//var num2 // 报错, 重复定义
var num3 int // 不报错, 因为作用域不同
fmt.Println("num3 = ", num3)
}
- C语言中全局变量没有赋值,那么默认初始值为0, 局部变量没有赋值,那么默认初始值是随机值
- Go语言中无论是全局变量还是局部变量,只要定义了一个变量都有默认的0值
- int/int8/int16/int32/int64/uint/uint8/uint16/uint32/uint64/byte/rune/uintptr的默认值是0
- float32/float64的默认值是0.0
- bool的默认值是false
- string的默认值是""
- pointer/function/interface/slice/channel/map/error的默认值是nil
- 其它复合类型array/struct默认值是内部数据类型的默认值
package main
import "fmt"
func main() {
var intV int // 整型变量
var floatV float32 // 实型变量
var boolV bool // 布尔型变量
var stringV string // 字符串变量
var pointerV \*int // 指针变量
var funcV func(int, int)int // function变量
var interfaceV interface{} // 接口变量
var sliceV []int // 切片变量
var channelV chan int // channel变量
var mapV map[string]string // map变量
var errorV error // error变量
fmt.Println("int = ", intV) // 0
fmt.Println("float = ", floatV) // 0
fmt.Println("bool = ", boolV) // false
fmt.Println("string = ", stringV) // ""
fmt.Println("pointer = ", pointerV) // nil
fmt.Println("func = ", funcV) // nil
fmt.Println("interface = ", interfaceV) // nil
fmt.Println("slice = ", sliceV) // []
fmt.Println("slice = ", sliceV == nil) // true
fmt.Println("channel = ", channelV) // nil
fmt.Println("map = ", mapV) // map[]
fmt.Println("map = ", mapV == nil) // true
fmt.Println("error = ", errorV) // nil
var arraryV [3]int // 数组变量
type Person struct{
name string
age int
}
var structV Person // 结构体变量
fmt.Println("arrary = ", arraryV) // [0, 0, 0]
fmt.Println("struct = ", structV) // {"" 0}
}
数据类型转换
- C语言中数据可以隐式转换或显示转换, 但是Go语言中数据
只能显示转换
- C语言隐式转换
#include <stdio.h>
int main(){
// 隐式转换:自动将实型10.6转换为整型后保存
int a = 10.6;
// 自动类型提升: 运算时会自动将小类型转换为大类型后运算
double b = 1.0 / 2; // 等价于1.0 / 2.0
}
- C语言显示转换(强制转换)
#include <stdio.h>
int main(){
// 显示转换:强制将实型10.6转换为整型后保存
int a = (int)10.5;
}
- Go语言数值类型之间转换
- 格式:
数据类型(需要转换的数据)
- 注意点: 和C语言一样数据可以从大类型转换为小类型, 也可以从小类型转换为大类型. 但是大类型转换为小类型可能会丢失精度
- 格式:
package main
import "fmt"
func main() {
var num0 int = 10
var num1 int8 = 20
var num2 int16
//num2 = num0 // 编译报错, 不同长度的int之间也需要显示转换
//num2 = num1 // 编译报错, 不同长度的int之间也需要显示转换
num2 = int16(num0)
num2 = int16(num1)
fmt.Println(num2)
var num3 float32 = 3.14
var num4 float64
//num4 = num3 // 编译报错, 不同长度的float之间也需要显示转换
num4 = float64(num3)
fmt.Println(num4)
var num5 byte = 11
var num6 uint8 // 这里不是隐式转换, 不报错的原因是byte的本质就是uint8
num6 = num5
fmt.Println(num6)
var num7 rune = 11
var num8 int32
num8 = num7 // 这里不是隐式转换, 不报错的原因是byte的本质就是int32
fmt.Println(num8)
}
数值类型和字符串类型之间转换
- Go语言中不能通过 数据类型(变量)的格式将数值类型转换为字符串, 也不能通过 数据类型(变量)的格式将字符串转换为数值类型
package main
import "fmt"
func main() {
var num1 int32 = 65
// 可以将整型强制转换, 但是会按照ASCII码表来转换
// 但是不推荐这样使用
var str1 string = string(num1)
fmt.Println(str1)
var num2 float32 = 3.14
// 不能将其它基本类型强制转换为字符串类型
var str2 string = string(num2)
fmt.Println(str2)
var str3 string = "97"
// 不能强制转换, cannot convert str2 (type string) to type int
var num3 int = int(str3)
fmt.Println(num3)
}
- 数值类型转字符串类型
strconv..FormatXxx()
package main
import "fmt"
func main() {
var num1 int32 = 10
// 第一个参数: 需要被转换的整型,必须是int64类型
// 第二个参数: 转换为几进制, 必须在2到36之间
// 将32位十进制整型变量10转换为字符串,并继续保留10进制格式
str1 := strconv.FormatInt(int64(num1), 10)
fmt.Println(str1) // 10
// 将32位十进制整型变量10转换为字符串,并转换为2进制格式
str2 := strconv.FormatInt(int64(num1), 2)
fmt.Println(str2) // 1010
var num5 float64 = 3.1234567890123456789
// 第一个参数: 需要转换的实型, 必须是float64类型
// 第二个参数: 转换为什么格式,f小数格式, e指数格式
// 第三个参数: 转换之后保留多少位小数, 传入-1按照指定类型有效位保留
// 第四个参数: 被转换数据的实际位数,float32就传32, float64就传64
// 将float64位实型,按照小数格式并保留默认有效位转换为字符串
str3 := strconv.FormatFloat(num5, 'f', -1, 64)
fmt.Println(str3) // 3.1234567
str4 := strconv.FormatFloat(num5, 'f', -1, 64)
fmt.Println(str4) // 3.1234567890123457
// 将float64位实型,按照小数格式并保留2位有效位转换为字符串
str5 := strconv.FormatFloat(num5, 'f', 2, 64)
fmt.Println(str5) // 3.12
// 将float64位实型,按照指数格式并保留2位有效位转换为字符串
str6 := strconv.FormatFloat(num5, 'e', 2, 64)
fmt.Println(str6) // 3.12
var num6 bool = true
str7 := strconv.FormatBool(num6)
fmt.Println(str7) // true
}
- 字符串类型转数值类型
strconv.ParseXxx()
package main
import "fmt"
func main() {
var str1 string = "125"
// 第一个参数: 需要转换的数据
// 第二个参数: 转换为几进制
// 第三个参数: 转换为多少位整型
// 注意点: ParseInt函数会返回两个值, 一个是转换后的结果, 一个是错误
// 如果被转换的数据转换之后没有超出指定的范围或者不能被转换时,
// 那么错误为nil, 否则错误不为nil
// 将字符串"125"转换为10进制的int8
num1, err := strconv.ParseInt(str1, 10, 8)
if err != nil {
fmt.Println(err)
}
fmt.Println(num1)
var str2 string = "150"
// 将字符串"150"转换为10进制的int8
// 由于int8的取值范围是-128~127, 所以转换之后超出了指定的范围, error不为nil
num2, err := strconv.ParseInt(str2, 10, 8)
if err != nil {
fmt.Println(err)
}
fmt.Println(num2)
var str3 string = "3.1234567890123456789"
// 第一个参数: 需要转换的数据
// 第二个参数: 转换为多少位小数, 32 or 64
// ParseFloat同样有两个返回值, 如果能够正常转换则错误为nil, 否则不为nil
num3, err := strconv.ParseFloat(str3, 32)
if err != nil {
// 例如: 把字符串"3.14abc"转换为小数就会报错, 因为"3.14abc"不是一个小数
fmt.Println(err)
}
fmt.Println(num3)
var str4 string = "true"
// 第一个参数: 需要转换的数据
// ParseBool同样有两个返回值, 如果能够正常转换则错误为nil, 否则不为nil
num4, \_ := strconv.ParseBool(str4)
fmt.Println(num4)
}
- 字符串类型转换为数值类型时,如果不能转换除了返回error以外,还会返回对应类型的默认值
package main
import "fmt"
func main() {
var str1 string = "abc"
num1, \_ := strconv.ParseInt(str1, 10, 32)
fmt.Println(num1) // 0
num2, \_ := strconv.ParseFloat(str1, 32)
fmt.Println(num2) // 0
num3, \_ := strconv.ParseBool(str1)
fmt.Println(num3) // false
}
- 看完上面的代码有没有种想打人的感觉? 如果有那么请继续往下看
- 字符串类型和整型快速转换
package main
import "fmt"
func main() {
var num1 int32 = 110
// 快速将整型转换为字符串类型
// 注意:Itoa方法只能接受int类型
var str1 string = strconv.Itoa(int(num1))
fmt.Println(str1)
var str2 string = "666"
// 快速将字符串类型转换为整型
// 注意: Atoi方法返回两个值, 一个值是int,一个值是error
// 如果字符串能被转换为int,那么error为nil, 否则不为nil
num2, err := strconv.Atoi(str2)
if err != nil{
fmt.Println(err)
}
fmt.Println(num2)
}
- 数值类型转字符串类型其它方式
package main
import "fmt"
func main() {
var num1 int32 = 110
// Sprintf函数和Printf函数很像, 只不过不是输出而将格式化的字符串返回给我们
var str1 string = fmt.Sprintf("%d", num1)
fmt.Println(str1)
var num2 float32 = 3.14
var str2 string = fmt.Sprintf("%f", num2)
fmt.Println(str2)
var num3 bool = true
var str3 string = fmt.Sprintf("%t", num3)
fmt.Println(str3)
}
Go语言常量
-
和C语言一样Go语言中的常量也分为
整型常量
、实型常量
、字符常量
、字符串常量
、自定义常量
-
自定义常量
- C语言自定义常量:
const 数据类型 常量名称 = 值;
- C语言自定义常量:
#include <stdio.h>
int main(int argc, const char \* argv[])
{
const float PI = 998;
PI = 110; // 报错
printf("PI = %d\n", PI );
return 0;
}
+ Go语言自定义常量: `const 常量名称 数据类型 = 值`or `const 常量名称 = 值`
package main
import "fmt"
func main() {
//const PI float32 = 3.14
//PI = 110 // 报错
//fmt.Println("PI = ", PI )
const PI = 3.14
PI = 110 // 报错
fmt.Println("PI = ", PI )
}
+ 除此之外Go语言还支持`一次性定义多个常量`
package main
import "fmt"
func main() {
// 多重赋值方式
const num1, num2 int = 100, 200
fmt.Println("num1 = ", num1)
fmt.Println("num2 = ", num2)
// 常量组方式
const (
num3 = 100
num4 = 200
)
fmt.Println("num3 = ", num3)
fmt.Println("num4 = ", num4)
// 常量组+多重赋值
const (
num5, num6 = 100, 200
num7 = 300
)
fmt.Println("num5 = ", num5)
fmt.Println("num6 = ", num6)
fmt.Println("num7 = ", num7)
}
- Go语言自定义常量注意点
- 定义的局部变量或者导入的包没有被使用, 那么编译器会报错,无法编译运行
- 但是定义的常量没有被使用,编译器不会报错, 可以编译运行
package main
import "fmt"
func main() {
// 可以编译运行
const PI float32 = 3.14
}
+ 在常量组中, 如果上一行常量有初始值,但是下一行没有初始值, 那么下一行的值就是上一行的值
package main
import "fmt"
func main() {
const (
num1 = 998
num2 // 和上一行的值一样
num3 = 666
num4 // 和上一行的值一样
num5 // 和上一行的值一样
)
fmt.Println("num1 = ", num1) // 998
fmt.Println("num2 = ", num2) // 998
fmt.Println("num3 = ", num3) // 666
fmt.Println("num4 = ", num4) // 666
fmt.Println("num5 = ", num5) // 666
const (
num1, num2 = 100, 200
num3, num4 // 和上一行的值一样, 注意变量个数必须也和上一行一样
)
fmt.Println("num1 = ", num1)
fmt.Println("num2 = ", num2)
fmt.Println("num3 = ", num3)
fmt.Println("num4 = ", num4)
}
- 枚举常量
- C语言中枚举类型的本质就是整型常量
- Go语言中没有C语言中明确意义上的enum定义, 但是可以借助iota标识符来实现枚举类型
- C语言枚举格式:
enum 枚举名 {
枚举元素1,
枚举元素2,
… …
};
- C语言枚举中,如果没有指定初始值,那么从0开始递增
#include <stdio.h>
int main(int argc, const char \* argv[])
{
enum Gender{
male,
female,
yao,
};
// enum Gender g = male;
// printf("%d\n", g); // 0
// enum Gender g = female;
// printf("%d\n", g); // 1
enum Gender g = yao;
printf("%d\n", g); // 2
return 0;
}
- C语言枚举中, 如果指定了初始值,那么从指定的数开始递增
#include <stdio.h>
int main(int argc, const char \* argv[])
{
enum Gender{
male = 5,
female,
yao,
};
// enum Gender g = male;
// printf("%d\n", g); // 5
// enum Gender g = female;
// printf("%d\n", g); // 6
enum Gender g = yao;
printf("%d\n", g); // 7
return 0;
}
- Go语言实现枚举格式
const(
枚举元素1 = iota
枚举元素2 = iota
... ...
)
- 利用iota标识符标识符实现从0开始递增的枚举
package main
import "fmt"
func main() {
const (
male = iota
female = iota
yao = iota
)
fmt.Println("male = ", male) // 0
fmt.Println("male = ", female) // 1
fmt.Println("male = ", yao) // 2
}
- iota注意点:
- 在同一个常量组中,iota从0开始递增,
每一行递增1
- 在同一个常量组中,只要上一行出现了iota,那么后续行就会自动递增
- 在同一个常量组中,iota从0开始递增,
package main
import "fmt"
func main() {
const (
male = iota // 这里出现了iota
female // 这里会自动递增
yao
)
fmt.Println("male = ", male) // 0
fmt.Println("male = ", female) // 1
fmt.Println("male = ", yao) // 2
}
+ 在同一个常量组中,如果iota被中断, 那么必须显示恢复
package main
import "fmt"
func main() {
const (
male = iota
female = 666 // 这里被中断, 如果没有显示恢复, 那么下面没有赋值的常量都和上一行一样
yao
)
fmt.Println("male = ", male) // 0
fmt.Println("male = ", female) // 666
fmt.Println("male = ", yao) // 666
}
package main
import "fmt"
func main() {
const (
male = iota
female = 666 // 这里被中断
yao = iota // 这里显示恢复, 会从当前常量组第一次出现iota的地方开始,每一行递增1, 当前是第3行,所以值就是2
)
fmt.Println("male = ", male) // 0
fmt.Println("male = ", female) // 666
fmt.Println("male = ", yao) // 2
}
+ iota也支持常量组+多重赋值, 在同一行的iota值相同
package main
import "fmt"
func main() {
const (
a, b = iota, iota
c, d = iota, iota
)
fmt.Println("a = ", a) // 0
fmt.Println("b = ", b) // 0
fmt.Println("c = ", c) // 1
fmt.Println("d = ", d) // 1
}
+ iota自增默认数据类型为int类型, 也可以显示指定类型
package main
import "fmt"
func main() {
const (
male float32 = iota // 显示指定类型,后续自增都会按照指定类型自增
female
yao
)
fmt.Printf("%f\n", male) // 0.0
fmt.Printf("%f\n", female) // 1.0
fmt.Printf("%f\n", yao) // 2.0
fmt.Println("male = ", reflect.TypeOf(female)) // float32
}
- Go语言fmt包实现了类似C语言printf和scanf的格式化I/O, 格式化动作源自C语言但更简单
##输出函数 - func Printf(format string, a …interface{}) (n int, err error)
- 和C语言用法几乎一模一样, 只不过新增了一些格式化符号
package main
import "fmt"
func main() {
name := "微信搜索:代码情缘"
age := 33
fmt.Printf("name = %s, age = %d\n", name, age) // name = lnj, age = 33
}
+ 值得注意的是,输出十进制只能通过%d,不能像C语言一样通过%i
+ 除了和C语言一样,可以通过%o、%x输出八进制和十六进制外,`还可以`直接通过%b输出二进制
package main
import "fmt"
func main() {
num := 15
fmt.Printf("十进制 = %d\n", num)
fmt.Printf("八进制 = %o\n", num)
fmt.Printf("十六进制 = %x\n", num)
fmt.Printf("二进制 = %b\n", num)
}
+ 除此之外,Go语言还增加了%T控制符, 用于输出值的类型
package main
import "fmt"
func main() {
type Person struct {
name string
age int
}
num1 := 10
num2 := 3.14
per := Person{"lnj", 33}
fmt.Printf("num1 = %T\n", num1) // int
fmt.Printf("num2 = %T\n", num2) // float64
fmt.Printf("per = %T\n", per) // main.Person
}
+ 除此之外,Go语言还增加了%v控制符,用于打印所有类型数据
- Go语言中输出某一个值,很少使用%d%f等, 一般都使用%v即可
- 输出复合类型时会自动生成对应格式后再输出
package main
import "fmt"
func main() {
type Person struct {
name string
age int
}
num1 := 10
num2 := 3.14
per := Person{"lnj", 33}
// 此时相当于把%v当做%d
fmt.Printf("num1 = %v\n", num1) // 10
// 此时相当于把%v当做%f
fmt.Printf("num2 = %v\n", num2) // 3.14
}
+ Go语言Printf函数其它特性,如宽度、标志、精度、长度、转移符号等,和C语言一样.
- func Println(a …interface{}) (n int, err error)
- 采用默认格式将其参数格式化并写入标准输出,
- 输出之后
会
在结尾处添加换行 - 传入多个参数时, 会自动在相邻参数之间添加空格
- 传入符合类型数据时, 会自动生成对应格式后再输出
- 输出之后
- 采用默认格式将其参数格式化并写入标准输出,
package main
import "fmt"
func main() {
num1 := 10
num2 := 3.14
fmt.Println(num1, num2) // 10 3.14
fmt.Println("num1 =", num1, "num2 =", num2) // num1 = 10 num2 = 3.14
type Person struct {
name string
age int
}
per := Person{"lnj", 33}
fmt.Println(per) // {lnj 33}
}
- func Print(a …interface{}) (n int, err error)
- 和Println几乎一样
- 输出之后
不会
在结尾处添加换行 - 传入多个参数时, 只有两个相邻的参数
都不是
字符串,才会在相邻参数之间添加空格 - 传入符合类型数据时, 会自动生成对应格式后再输出
- 输出之后
- 和Println几乎一样
package main
import "fmt"
func main() {
num1 := 10
num2 := 3.14
fmt.Print(num1, num2) // 10 3.14
fmt.Print("num1 =", num1, "num2 =", num2) // num1 =10 num2 =3.14
type Person struct {
name string
age int
}
per := Person{"lnj", 33}
fmt.Print(per) // {lnj 33}
}
- 以下三个函数和Printf/Println/Print函数一样, 只不过上面三个函数是输出到标准输出, 而下面三个函数可以通过w指定输出到什么地方
- func Fprintf(w io.Writer, format string, a …interface{}) (n int, err error)
- func Fprintln(w io.Writer, a …interface{}) (n int, err error)
- func Fprint(w io.Writer, a …interface{}) (n int, err error)
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
// os.Stdout 写入到标准输出
name := "lnj"
age := 33
// 第一个参数: 指定输出到什么地方
// 第二个参数: 指定格式控制字符串
// 第三个参数: 指定要输出的数据
fmt.Fprintf(os.Stdout, "name = %s, age = %d\n", name, age)
// http.ResponseWriter 写入到网络响应
http.HandleFunc("/", func(writer http.ResponseWriter, request \*http.Request) {
fmt.Fprintf(writer, "name = %s, age = %d\n", name, age)
})
http.ListenAndServe(":8888", nil)
}
- 以下三个函数和Printf/Println/Print函数一样, 只不过上面三个函数是输出到标准输出,
而下面三个函数不会输出,而是将字符串返回给我们 - func Sprintf(format string, a …interface{}) string
- func Sprint(a …interface{}) string
- func Sprintln(a …interface{}) string
package main
import (
"fmt"
"net/http"
"os"
)
func main() {
name := "lnj"
age := 33
// 按照指定的格式生成字符串
str := fmt.Sprintf("name = %s, age = %d\n", name, age)
// 输出生成的字符串
fmt.Println(str)
}
输入函数
- func Scanf(format string, a …interface{}) (n int, err error)
- 和C语言用法几乎一模一样, 但是只能输入一行数据
package main
import "fmt"
func main() {
var num1 int
var num2 int
fmt.Scanf("%d%d", &num1, &num2)
fmt.Println(num1, num2)
}
- func Scan(a …interface{}) (n int, err error)
- 和C语言scanf函数几乎一样, 只不过不用指定格式化字符串
package main
import "fmt"
func main() {
var num1 int
var num2 int
fmt.Scan(&num1, &num2)
fmt.Println(num1, num2)
var num3 float32
var num4 float32
fmt.Scan(&num3, &num4)
fmt.Println(num3, num4)
}
- func Scanln(a …interface{}) (n int, err error)
- 和C语言用法几乎一模一样, 只不过不用指定格式化字符串, 并且只能输入一行数据
package main
import "fmt"
func main() {
var num1 int
var num2 int
fmt.Scanln(&num1, &num2)
fmt.Println(num1, num2)
}
- 以下三个函数和Scan/Scanln/Scanf函数一样, 只不过上面三个函数是从标准输入读取数据, 而下面三个函数可以通过r指定从哪读取数据
- func Fscanf(r io.Reader, format string, a …interface{}) (n int, err error)
- func Fscanln(r io.Reader, a …interface{}) (n int, err error)
- func Fscan(r io.Reader, a …interface{}) (n int, err error)
package main
import "fmt"
func main() {
var num1 int
var num2 int
// 第一个参数: 指定从哪读取数据
// 第二个参数: 指定格式控制字符串
// 第三个参数: 指定要输出的数据
fmt.Fscanf(os.Stdin, "%d%d", &num1, &num2)
fmt.Println(num1, num2)
s := strings.NewReader("lnj 33")
var name string
var age int
// 从指定字符串中扫描出想要的数据
// 注意:
fmt.Fscanf(s, "%s%d", &name, &age)
fmt.Println("name =",name, "age =",age)
}
- 以下三个函数和Scan/Scanln/Scanf函数一样, 只不过上面三个函数是从标准输入读取数据, 而下面三个函数是从字符串中读取数据
- func Sscan(str string, a …interface{}) (n int, err error)
- func Sscanf(str string, format string, a …interface{}) (n int, err error)
- func Sscanln(str string, a …interface{}) (n int, err error)
package main
import "fmt"
func main() {
str := "lnj 33"
var name string
var age int
//fmt.Sscanf(str, "%s %d",&name, &age)
//fmt.Sscanln(str,&name, &age)
fmt.Sscan(str,&name, &age)
fmt.Println("name =",name, "age =",age)
}
go命令行操作指令
- 标准go语言项目文件目录格式
- 项目文件夹就是GOPATH指向的文件夹
- src文件夹是专门用于存放源码文件的
- main文件夹是专门用于存储package main包相关源码文件的
- 其它文件夹是专门用于存储除package main包以外源码文件的
- bin文件夹是专门用于存储编译之后的可执行程序的
- pag文件夹是专门用于存储编译之后的.a文件的
|—项目文件夹
-----------|--------src文件夹
-----------------------------|--------main文件夹
-----------------------------|--------其它文件夹
-----------|--------bin文件夹
-----------|--------pkg文件夹
go version
查看当前安装的go版本go env
查看当前go的环境变量go fmt
格式化代码- 会将指定文件中凌乱的代码按照go语言规范格式化
go run 命令文件
编译并运行go程序- package main包中包含main函数的文件, 我们称之为命令文件
- 其它包中的文件, 我们称之为源码文件
go build
编译检查- 对于非命令文件只会执行编译检查, 不会产生任何文件
- 对于命令文件除了编译检查外,还会在当前目录下生成一个可执行文件
- 对应只想编译某个文件, 可以在命令后面指定文件名称
go build 文件名称
go install
安装程序- 对于非命令文件会执行编译检查, 并生成.a结尾的包, 放到 $GOPATH/pkg目录中
- 对于命令文件会执行编译检查, 并生成可执行程序, 放到$GOPATH/bin目录中
通过os包获取命令行参数
- C语言中的命令行参数
- argc: argv中保存数据的个数
- argv: 默认情况下系统只会传入一个值, 这个值就是main函数执行文件的路径
- 我们可以通过配置开发工具,或者命令行运行时以
空格+参数
形式传递其它参数 - 注意点: 无论外界传入的是什么类型, 我们拿到的都是
字符串类型
#include <stdio.h>
int main(int argc, const char \* argv[])
{
for(int i = 0; i < argc; i++){
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
- Go语言中的命令行参数
- Go语言中main函数没有形参, 所以不能直接通过main函数获取命令行参数
- 想要获取命令行参数必须导入os包, 通过os包的Args获取
- 注意点: 无论外界传入的是什么类型, 我们拿到的都是
字符串类型
package main
import (
"fmt"
"os" // 用于获取命令行参数的包
)
func main() {
// 1.获取传入参数个数
num := len(os.Args)
// 2.打印所有获取到的参数
for i := 0; i < num; i++ {
fmt.Println(os.Args[i])
}
}
通过flag包获取命令行参数
- Go语言中除了可以通过os包获取命令行参数以外,还可以通过flag包获取命令行参数
package main
import (
"flag"
"fmt"
)
func main() {
/\*
flag.Xxxx(name, value, usage)
第一个参数: 命令行参数名称
第二个参数: 命令行参数对应的默认值
第三个参数: 命令行参数对应的说明
\*/
// 1.设置命令行参数
name := flag.String("name", "lnj", "请输入人的姓名")
age := flag.Int("age", 33, "请输入人的年龄")
// 2.将命令行参数解析到注册的参数
flag.Parse()
// 3.使用命令行参数
// 注意flag对应方法返回的都是指针类型, 所以使用时必须通过指针访问
fmt.Println("name = ", \*name)
fmt.Println("age = ", \*age)
}
- flag获取命令行参数第二种写法
package main
import (
"flag"
"fmt"
)
func main() {
/\*
flag.Xxxx(\*type, name, value, usage)
第一个参数:保存命令行参数变量地址
第二个参数: 命令行参数名称
第三个参数: 命令行参数对应的默认值
第四个参数: 命令行参数对应的说明
\*/
// 1.定义变量,保存命令行参数的值
var name string
var age int
// 2.设置命令行参数
flag.StringVar(&name, "name", "lnj", "请输入人的姓名")
flag.IntVar(&age, "age", 33,"请输入人的姓名")
// 3.注册解析命令行参数
flag.Parse()
// 4.使用命令行参数
fmt.Println("name = ", name)
fmt.Println("age = ", age)
}
os包和flag包获取命令行参数对比
- 通过os包获取命令行参数
- 如果用户没有传递参数
会
报错 需要
严格按照代码中的顺序传递参数, 否则会
造成数据混乱不能
指定参数的名称- 获取到的数据都是
字符串
类型
- 如果用户没有传递参数
package main
import (
"os"
"fmt"
)
int main(){
name := os.Args[1]
age := os.Args[2]
fmt.Println("name = ", name)
fmt.Println("age = ", age)
}
-
通过flag包获取命令行参数
- 如果用户没有传递参数
不会
报错 不需要
严格按照代码中的顺序传递参数,不会
造成数据混乱可以
指定参数的名称- 获取到的数据是我们自己指定的类型
- 如果用户没有传递参数
package main
import (
"flag"
"fmt"
)
int main(){
name := flag.String("name", "lnj", "请输入人的姓名")
age := flag.Int("age", 33, "请输入人的年龄")
// 2.注册解析命令行参数
flag.Parse()
// 3.使用命令行参数
// 注意flag对应方法返回的都是指针类型, 所以使用时必须通过指针访问
fmt.Println("name = ", \*name)
fmt.Println("age = ", \*age)
}
算数运算符
- 算数运算符和C语言几乎一样
运算符 | 描述 | 实例 |
---|---|---|
+ | 相加 | A + B |
- | 相减 | A - B |
* | 相乘 | A * B |
/ | 相除 | B / A |
% | 求余 | B % A |
++ | 自增 | A++ |
– | 自减 | A– |
- 注意点:
- 只有相同类型的数据才能进行运算
package main
import "fmt"
int main(){
var num1 int32 = 10
//var num2 int64 = num1 // 类型不同不能进行赋值运算
var num2 int64 = int64(num1) // 类型不同不能进行赋值运算
fmt.Println(num2)
var num3 int32 = 10
var num4 int64 = 20
//var res int64 = num3 + num4 // 类型不同不能进行算数运算
var res1 int64 = int64(num3) + num4 // 类型不同不能进行算数运算
fmt.Println(res1)
var num5 int32 = 10
var num6 int64 = 20
//var res2 bool = (num5 == num6) // 类型不同不能进行关系运算
var res2 bool = (num5 == int32(num6)) // 类型不同不能进行关系运算
fmt.Println(res2)
// ... ... 其它以此类推
}
- Go语言中++、–运算符不支持前置
- 错误写法: ++i; --i;
- Go语言中++、–是语句,不是表达式,所以必须独占一行
- 错误写法: a = i++; return i++;
package main
import "fmt"
func main() {
num1 := 0
num1++
fmt.Println(num1)
//++num1 // 编译报错, Go语言中++只能后置,不能前置
//fmt.Println(num1)
//var num2 int = num1++ // 编译报错, num1++是语句不是表达式, 所以必须独占一行
//fmt.Println(num2)
}
- Go语言中字符串支持利用+号进行拼接
package main
import "fmt"
func main() {
str := "abc" + "def"
//fmt.Println(str)
}
关系算符
- 关系算符和C语言一样
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个值是否相等,如果相等返回 True 否则返回 False。 | A == B |
!= | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 | A != B |
> | 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 | A > B |
< | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 | A < B |
>= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 | A >= B |
<= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 | A <= B |
- 注意点:
- 和C语言不通的是, Go语言中关系运算符只能返回true和false
逻辑运算符
- 逻辑运算符和C语言一样
运算符 | 描述 | 实例 |
---|---|---|
&& | 如果两边的操作数都是 True,则条件 True,否则为 False。 | A && B |
|| | 如果两边的操作数有一个 True,则条件 True,否则为 False。 | A |
! | 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 | !A |
- 注意点:
- 和C语言不通的是, Go语言中关系运算符只能返回true和false
- 逻辑非只能用于true和false
位运算符
- 位运算符和C语言几乎一样
运算符 | 描述 | 实例 |
---|---|---|
& | 参与运算的两数各对应的二进位相与, 对应位只要都是1结果就为1 | A & B |
| | 参与运算的两数各对应的二进位相或,对应位只要其中一个是1结果就为1 | A |
^ | 参与运算的两数各对应的二进位相异或,对应位只要不同结果就是1 | A ^ B |
<< | 左移运算符,左移n位就是乘以2的n次方 | A << 2 |
>> | 右移运算符,右移n位就是除以2的n次方 | B >> 2 |
&^ | 逻辑清零运算符, B对应位是1,A对应位清零,B对应位是0, A对应位保留原样 | A &^ B |
- 新增一个&^运算符
int main(){
/\*
0110 a
&^1011 b 如果b位位1,那么结果为0, 否则结果为a位对应的值
----------
0100
\*/
a1 := 6
b1 := 11
res1 := a1 &^ b1
fmt.Println("res1 = ", res1) // 4
/\*
1011 a
&^1101 b 如果b位位1,那么结果为0, 否则结果为a位对应的值
----------
0010
\*/
a2 := 11
b2 := 13
res2 := a2 &^ b2
fmt.Println("res2 = ", res2) // 2
}
赋值运算符
- 赋值运算符和C语言几乎一样
- 新增一个&^=运算符
运算符 | 描述 | 实例 |
---|---|---|
= | 将右边赋值给左边 | 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 <<= 2 等于 C = C << 2 |
>>= | 右移赋值 | C >>= 2 等于 C = C >> 2 |
&= | 位逻辑与赋值 | C &= 2 等于 C = C & 2 |
^= | 位逻辑或赋值 | C ^= 2 等于 C = C ^ 2 |
|= | 位逻辑异或赋值 | C |
&^= | 位逻辑清零赋值 | C &^= 2 等于 C = C &^ 2 |
其它运算符
运算符 | 描述 | 实例 |
---|---|---|
& | 返回变量存储地址 | &a; 将给出变量的实际地址 |
* | 访问指针指向内存 | *p; 访问指针p指向内存 |
package main
import "fmt"
int main(){
var num int = 666
var p \*int = &num
fmt.Println(num)
fmt.Println(\*p)
num = 777
fmt.Println(num)
\*p = 999
fmt.Println(num)
}
- 注意点
- 指针类型只支持相等运算, 不能做加减运算
#include <stdio.h>
int main()
{
int ages[3] = {19, 23, 22};
int \*arrayP = &ages[0];
printf("ages[0] = %i\n", \*(arrayP + 0)); // \*(arrayP + 0) == \*arrayP
printf("ages[1] = %i\n", \*(arrayP + 1));
printf("ages[2] = %i\n", \*(arrayP + 2));
return 0;
}
package main
import "fmt"
int main(){
var ages [3]int = [3]int{19, 23, 22}
var p \*int = &ages[0]
//fmt.Println(&ages[0])
//fmt.Println(\*p) // 19
fmt.Println(\*(p + 0)) // 编译报错
}
运算符优先级
- 和C语言一样, 只需记住()优先级最高即可
Go语言流程控制基本概念
- Go语言流程控制和C语言一样, 也有三大流程控制结构
- 顺序结构(默认结构)
- 选择结构(if / switch)
- 循环结构(for)
选择结构if
- 和C语言不同的的是
- 条件表达式的值必须是布尔类型(Go语言中没有非零即真的概念)
- 条件表达式前面可以添加初始化语句
- 不需要编写圆括号
- 左大括号必须和条件语句在同一行
- 第一种格式:
- 条件表达式结果为true,那么执行if后面{}中代码
if 初始化语句; 条件表达式{
语句块;
}
package main
import "fmt"
func main() {
// 如果后续需要用到age变量, 可以将变量放到if外面
age := 18
if age >= 18{
fmt.Println("成年人")
}
}
package main
import "fmt"
func main() {
// 如果后续不需要用到age变量, 可以将变量放到条件表达式前面
if age := 18; age >= 18{
fmt.Println("成年人")
}
}
- 第二种格式:
- 条件表达式结果为true,那么执行if后面{}中代码
- 否则执行else后面{}中代码
if 初始化语句; 条件表达式{
语句块;
}else{
语句块;
}
package main
import "fmt"
func main() {
if age := 18;age >= 18 {
fmt.Println("成年人")
}else{
fmt.Println("未成年人")
}
}
- 第三种格式:
- if后面条件表达式结果为true,那么执行if后面{}中代码
- 否则判断else if条件表达式是否为true,为true执行else if后面{}中代码
- 否则依次判断后续else if条件表达式是否为true,哪个为true就执行哪个else if后面{}中代码
- 都不满足则执行else后面{}中代码
if 初始化语句; 条件表达式{
语句块;
}else if 条件表达式{
语句块;
}
... ...
else{
语句块;
}
package main
import "fmt"
func main() {
if age := 18;age > 55{
fmt.Println("老年人")
}else if age >= 40{
fmt.Println("中年人")
}else if age >= 18{
fmt.Println("成年人")
}else{
fmt.Println("未成年人")
}
}
- 值得一提的是Go语言中没有C语言中的三目运算符, 所以C语言中三目能干的在Go语言中都只能通过if else的形式来完成
选择结构switch
- 和C语言不同的的是
- 和if一样,表达式前面可以添加初始化语句
- 和if一样,不需要编写圆括号
- 和if一样,左大括号必须和表达式在同一行
- case表达式的值不一定要是常量, 甚至可以不用传递
- 一个case后面可以有多个表达式, 满足其中一个就算匹配
- case后面不需要添加break
- 可以在case语句块最后添加fallthrough,实现case穿透
- case后面定义变量不需要添加{}明确范围
- 格式
switch 初始化语句; 表达式{
case 表达式1, 表达式2:
语句块;
case 表达式1, 表达式2:
语句块;
default:
语句块;
}
package main
import "fmt"
func main() {
switch num := 3;num {
case 1:
fmt.Println("星期一")
case 2:
fmt.Println("星期二")
case 3:
fmt.Println("星期三")
case 4:
fmt.Println("星期四")
case 5:
fmt.Println("星期五")
case 6:
fmt.Println("星期六")
case 7:
fmt.Println("星期日")
default:
fmt.Println("Other...")
}
}
package main
import "fmt"
func main() {
switch num := 3;num {
case 1,2,3,4,5:
fmt.Println("工作日")
case 6,7:
fmt.Println("非工作日")
default:
fmt.Println("Other...")
}
}
- 注意点:
- case后面不用编写break, 不会出现case穿透问题
- 如果想让case穿透,必须在case语句块最后添加fallthrough关键
package main
import "fmt"
func main() {
switch num := 3;num {
case 1:
fallthrough
case 2:
fallthrough
case 3:
fallthrough
case 4:
fallthrough
case 5:
fmt.Println("工作日")
case 6:
fallthrough
case 7:
fmt.Println("非工作日")
default:
fmt.Println("Other...")
}
}
- case后面不仅仅可以放常量,还可以放变量和表达式
package main
import "fmt"
func main() {
value := 2
switch num:=1; num {
case value: // 变量
fmt.Println("num等于value")
default:
fmt.Println("num不等于value")
}
}
package main
import "fmt"
func main() {
value := 2
switch num:=1; num {
case getValue(): // 函数
fmt.Println("num等于value")
default:
fmt.Println("num不等于value")
}
}
func getValue() int {
return 1
}
package main
import "fmt"
func main() {
switch num:=18; {
case num >=0 && num <=10: // 表达式
fmt.Println("num是一个0~10之间的数")
case num >10 && num <=20:
fmt.Println("num是一个11~20之间的数")
default:
fmt.Println("num是一个大于20的数")
}
}
- case后面定义变量不用添加{}明确作用于范围
package main
import "fmt"
func main() {
switch num := 1;num {
case 1:
value := 10 // 不会报错
fmt.Println(value)
default:
fmt.Println("Other...")
}
}
- 其它特性和C语言一样
循环结构for
- 和C语言不同的的是
- 和if一样,条件表达式的值必须是布尔类型
- 和if一样,不需要编写圆括号
- 和if一样,左大括号必须和表达式在同一行
- 格式:
for 初始化表达式;循环条件表达式;循环后的操作表达式 {
循环体语句;
}
package main
import "fmt"
func main() {
for i:=0; i<10; i++{
fmt.Println(i)
}
}
- Go语言中没有while/dowhile循环, 所以可以通过如下格式实现C语言中while循环用法
package main
import "fmt"
func main() {
i:=0
for i<10 {
fmt.Println(i)
i++
}
}
- 最简单死循环
package main
import "fmt"
func main() {
for{
fmt.Println("根本停不下来")
}
}
- 除了实现基本的循环结构以外,Go语言还实现了一种高级for循环
for...range循环
for...range循环
可以快速完成对字符串、数组、slice、map、channel遍历
- 格式
for 索引, 值 := range 被遍历数据{
}
package main
import "fmt"
func main() {
// 1.定义一个数组
arr := [3]int{1, 3, 5}
// 2.快速遍历数组
// i用于保存当前遍历到数组的索引
// v用于保存当前遍历到数组的值
for i, v := range arr{
fmt.Println(i, v)
}
}
四大跳转
- 和C语言一样,Go语言中也有四大跳转语句, 分别是return、break、continue、goto
- break语句
- Go语言中的break语句可以用于,立即跳出switch、for和select
- 但不同的是Go语言中的break语句可以指定标签
package main
import "fmt"
func main() {
for i:=0; i<10; i++{
if(i == 5){
break // 跳出所在循环
}
fmt.Println(i)
}
}
- 利用break跳转到指定标签
- 标签必须在使用之前定义
- 标签后面只能跟switch和循环语句, 不能插入其它语句
- 跳转到标签之后switch和循环不会再次被执行
package main
import "fmt"
func main() {
outer:
switch num:=2; num {
case 1:
fmt.Println(1)
case 2:
fmt.Println(2)
break outer
default:
fmt.Println("other")
}
fmt.Println("come here")
}
package main
import "fmt"
func main() {
outer:
for i:=0; i<5; i++{
for j:=0; j<10; j++ {
if (j == 5) {
break outer// 跳出到指定标签
}
fmt.Println(j)
}
}
fmt.Println("come here")
}
- continue语句
- Go语言中的continue语句可以用于,立即进入下一次循环
- 但不同的是Go语言中的continue语句可以指定标签
package main
import "fmt"
func main() {
for i:=0; i<5; i++{
if (i == 2) {
continue
}
fmt.Println(i)
}
}
- 利用continue 跳转到指定标签
- 标签必须在使用之前定义
- 标签后面只能跟循环语句, 不能插入其它语句
- 对于单层循环和直接编写continue一样
- 对于多层循环,相当于跳到最外层循环继续判断条件执行
package main
import "fmt"
func main() {
outer:
for i:=0; i<5; i++{
fmt.Println("i =", i) // 0 1 2 3 4
for j:=0; j<10; j++ {
if (j == 5) {
continue outer// 跳出到指定标签
}
}
}
fmt.Println("come here")
}
- goto语句
- Go语言中的goto和C语言中用法一样, 用于在同一个函数中瞎跳
package main
import "fmt"
func main() {
num := 1
outer:
if(num <= 10){
fmt.Println(num)
num++
goto outer // 死循环
}
fmt.Println("come here")
}
package main
import "fmt"
func main() {
num := 1
if(num <= 10){
fmt.Println(num)
num++
goto outer // 死循环
}
outer:
fmt.Println("come here")
}
- Go语言中的return语句和C语言一模一样,都是用于结束函数,将结果返回给调用者
函数
- Go语言和C语言一样也有函数的概念, Go语言中函数除了定义格式和不用声明以外,其它方面几乎和C语言一模一样
- 格式:
func 函数名称(形参列表)(返回值列表){
函数体;
}
- 无参数无返回值函数
func say() {
fmt.Println("Hello World!!!")
}
- 有参数无返回值函数
func say(name string) {
fmt.Println("Hello ", name)
}
- 无参数有返回值函数
func sum() int { // 只有一个返回值时,返回值列表的()可以省略
return 1 + 1
}
- 有参数有返回值函数
func sum(a int, b int) int {
return a + b
}
和C语言函数差异
- 和C语言不同的是,Go语言中可以给函数的返回值指定名称
// 给返回值指定了一个名称叫做res, return时会自动将函数体内部res作为返回值
// 其实本质就是提前定义了一个局部变量res, 在函数体中使用的res就是这个局部变量,返回的也是这个局部变量
func sum() (res int) {
res = 1 + 1
return
}
- 和C语言不同的是,Go语言中的函数允许有多个返回值函数
func calculate(a int, b int) (sum int, sub int) {
sum = a + b
sub = a - b
return
}
- 相邻同类型形参OR返回值类型可以合并, 可以将数据类型写到最后一个同类型形参OR返回值后面
// a, b都是int类型, 所以只需要在b后面添加int即可
func calculate(a, b int) (sum, sub int) {
sum = a + b
sub = a - b
return
}
- 和C语言不同的是Go语言中的函数不需要先声明在使用
package main
import "fmt"
func main() {
say();
}
func say() { // 在后面定义也可以在前面使用
fmt.Println("Hello World!!!")
}
值传递和引用传递
- Go语言中
值类型
有: int系列、float系列、bool、string、数组、结构体- 值类型通常在栈中分配存储空间
- 值类型作为函数参数传递, 是拷贝传递
- 在函数体内修改值类型参数, 不会影响到函数外的值
package main
import "fmt"
func main() {
num := 10
change(num)
fmt.Println(num) // 10
}
func change(num int) {
num = 998
}
package main
import "fmt"
func main() {
arr := [3]int{1, 3, 5}
change(arr)
fmt.Println(arr) // 1, 3, 5
}
func change(arr [3]int) {
arr[1] = 8
}
package main
import "fmt"
type Person struct {
name string
age int
}
func main() {
p := Person{"lnj", 33}
change(p)
fmt.Println(p.name) // lnj
}
func change(p Person) {
p.name = "zs"
}
- Go语言中
引用类型
有: 指针、slice、map、channel- 引用类型通常在堆中分配存储空间
- 引用类型作为函数参数传递,是引用传递
- 在函数体内修改引用类型参数,会影响到函数外的值
package main
import "fmt"
func main() {
num := 10
change(&num)
fmt.Println(num) // 998
}
func change(num \*int) {
\*num = 998
}
package main
import "fmt"
func main() {
arr := []int{1, 3, 5}
change(arr)
fmt.Println(arr) // 1, 8, 5
}
func change(arr []int) {
arr[1] = 8
}
package main
import "fmt"
func main() {
mp := map[string]string{"name":"lnj", "age":"33"}
change(mp)
fmt.Println(mp["name"]) // zs
}
func change(mp map[string]string) {
mp["name"] = "zs"
}
匿名函数
- 匿名函数也是函数的一种, 它的格式和普通函数一模一样,只不过没有名字而已
- 普通函数的函数名称是固定的, 匿名函数的函数名称是系统随机的
- 匿名函数可以定义在函数外(全局匿名函数),也可以定义在函数内(局部匿名函数), Go语言中的普通函数不能嵌套定义, 但是可以通过匿名函数来实现函数的嵌套定义
- 全局匿名函数
package main
import "fmt"
// 方式一
var a = func() {
fmt.Println("hello world1")
}
// 方式二
var (
b = func() {
fmt.Println("hello world2")
}
)
func main() {
a()
b()
}
- 一般情况下我们很少使用全局匿名函数, 大多数情况都是使用局部匿名函数, 匿名函数可以直接调用、保存到变量、作为参数或者返回值
- 直接调用
package main
import "fmt"
func main() {
func(s string){
fmt.Println(s)
}("hello lnj")
}
- 保存到变量
package main
import "fmt"
func main() {
a := func(s string) {
fmt.Println(s)
}
a("hello lnj")
}
- 作为参数
package main
import "fmt"
func main() {
test(func(s string) {
fmt.Println(s)
})
}
func test(f func(s string)) {
f("hello lnj")
}
- 作为返回值
package main
import "fmt"
func main() {
res := test()
res(10, 20)
}
func test() func(int, int) {
return func(a int, b int) {
fmt.Println(a + b)
}
}
- 匿名函数应用场景
- 当某个函数只需要被调用一次时, 可以使用匿名函数
- 需要执行一些不确定的操作时,可以使用匿名函数
package main
import "fmt"
func main() {
// 项目经理的一天
work(func() {
fmt.Println("组织部门开会")
fmt.Println("给部门员工分配今天工作任务")
fmt.Println("检查部门员工昨天提交的代码")
fmt.Println("... ...")
})
// 程序员的一天
work(func() {
fmt.Println("参加部门会议")
fmt.Println("修改测试提交的BUG")
fmt.Println("完成老大今天安排的任务")
fmt.Println("... ...")
})
}
// 假设我们需要编写一个函数,用于描述一个人每天上班都需要干嘛
// 职场中的人每天上班前,上班后要做的事几乎都是相同的, 但是每天上班过程中要做的事确实不确定的
// 所以此时我们可以使用匿名函数来解决, 让上班的人自己觉得自己每天上班需要干什么
func work(custom func()) {
// 上班前
fmt.Println("起床")
fmt.Println("刷牙")
fmt.Println("洗脸")
fmt.Println("出门")
fmt.Println("上班打卡")
fmt.Println("开电脑")
// 上班中
custom()
// 上班后
fmt.Println("关电脑")
fmt.Println("下班打卡")
fmt.Println("出门")
fmt.Println("到家")
fmt.Println("吃饭")
fmt.Println("睡觉")
}
- 为了提升代码的可读性,我们还可以将这个大函数拆解为独立的匿名函数
func work(custom func()) {
// 这种写法的好处是代码层次清晰,并且如果有一些变量
// 只需要在上班前或上班后使用,还可以将这些变量隔离,不对外界造成污染
// 上班前
func(){
fmt.Println("起床")
fmt.Println("刷牙")
fmt.Println("洗脸")
fmt.Println("出门")
fmt.Println("上班打卡")
fmt.Println("开电脑")
}()
// 上班中
custom()
// 上班后
func(){
fmt.Println("关电脑")
fmt.Println("下班打卡")
fmt.Println("出门")
fmt.Println("到家")
fmt.Println("吃饭")
fmt.Println("睡觉")
}()
}
func work(custom func()) {
// 前提条件是这个函数只在work函数中使用, 两者有较强的关联性, 否则建议定义为普通函数
pre := func(){
fmt.Println("起床")
fmt.Println("刷牙")
fmt.Println("洗脸")
fmt.Println("出门")
fmt.Println("上班打卡")
fmt.Println("开电脑")
}
latter := func(){
fmt.Println("关电脑")
fmt.Println("下班打卡")
fmt.Println("出门")
fmt.Println("到家")
fmt.Println("吃饭")
fmt.Println("睡觉")
}
// 上班前
pre()
// 上班中
custom()
// 上班后
latter()
}
闭包
- 闭包是一个特殊的匿名函数, 它是匿名函数和相关引用环境组成的一个整体
- 也就是说只要匿名函数中用到了外界的变量, 那么这个匿名函数就是一个闭包
package main
import "fmt"
func main() {
num := 10
a := func() {
num++ // 在闭包中用到了main函数中的num, 所以这个匿名函数就是一个闭包
fmt.Println(num) // 11
}
a()
}
+ 闭包中使用的变量和外界的变量是同一个变量, 所以可以闭包中可以修改外界变量
package main
import "fmt"
func main() {
num := 10
a := func() {
num = 6 // 在闭包中用到了main函数中的num, 所以这个匿名函数就是一个闭包
fmt.Println(num) // 6
}
fmt.Println("执行闭包前", num) // 10
a()
fmt.Println("执行闭包后", num) // 6
}
+ 只要闭包还在使用外界的变量, 那么外界的变量就会一直存在
package main
import "fmt"
func main() {
res := addUpper() // 执行addUpper函数,得到一个闭包
fmt.Println(res()) // 2
fmt.Println(res()) // 3
fmt.Println(res()) // 4
fmt.Println(res()) // 5
}
func addUpper() func() int {
x := 1
return func() int {
x++ // 匿名函数中用到了addUpper中的x,所以这是一个闭包
return x
}
}
延迟调用
- Go语言中没有提供其它面向对象语言的析构函数, 但是Go语言提供了defer语句用于实现其它面向对象语言析构函数的功能
- defer语句常用于
释放资源
、解除锁定
以及错误处理
等- 例如C语言中我们申请了一块内存空间,那么不使用时我们就必须释放这块存储空间
- 例如C语言中我们打开了一个文件,那么我们不使用时就要关闭这个文件
- 例如C语言中我们打开了一个数据库, 那么我们不使用时就要关闭这个数据库
- 这一类的操作在Go语言中都可以通过defer语句来完成
- 无论你在什么地方注册defer语句,它都会在所属函数执行完毕之后才会执行, 并且如果注册了多个defer语句,那么它们会按照
后进先出
的原则执行- 正是因为defer语句的这种特性, 所以在Go语言中关闭资源不用像C语言那样用完了再关闭, 我们完全可以打开的同时就关闭, 因为无论如何defer语句都会在所属函数执行完毕之后才会执行
package main
import "fmt"
func main() {
defer fmt.Println("我是第一个被注册的") // 3
fmt.Println("main函数中调用的Println") // 1
defer fmt.Println("我是第二个被注册的") // 2
}
init函数
- golang里面有两个保留的函数:
- init函数(能够应用于所有的package)
- main函数(只能应用于package main)
- 这两个函数在定义时不能有任何的参数和返回值
- go程序会自动调用init()和main(),所以你
不能
在任何地方调用这两个函数 - package main必须包含一个main函数, 但是每个package中的init函数都是可选的
- 一个package里面可以写任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们都强烈建议用户在一个package中每个文件只写一个init函数
- 单个包中代码执行顺序如下
main包-->常量-->全局变量-->init函数-->main函数-->Exit
package main
import "fmt"
const constValue = 998 // 1
var gloalVarValue int = abc() // 2
func init() { // 3
fmt.Println("执行main包中main.go中init函数")
}
func main() { // 4
fmt.Println("执行main包中main.go中main函数")
}
func abc() int {
fmt.Println("执行main包中全局变量初始化")
return 998
}
-
多个包之间代码执行顺序如下
-
init函数的作用
- init函数用于处理当前文件的初始化操作, 在使用某个文件时的一些准备工作应该放到这里
数组
- 和C语言一样,Go语言中也有数组的概念, Go语言中的数组也是用于保存一组
相同类型
的数据 - 和C语言一样,Go语言中的数组也分为
一维数组
和多维数组
一维数组
- 格式:
var arr [元素个数]数据类型
- 和C语言中数组不同, Go语言中数组定义之后就
有默认的初始值
- 默认初始值就是保存数据类型的默认值(零值)
- 和C语言中数组不同, Go语言中数组定义之后就
package main
import "fmt"
func main() {
// 1.定义一个数组
var arr [3]int
// 2.打印数组
fmt.Println(arr) //[0 0 0]
// 1.定义一个数组
var arr [3]bool
// 2.打印数组
fmt.Println(arr) //[false false false]
}
+ 和C语言一样,Go语言中的数组也提供了好几种初始化方式
package main
import "fmt"
func main() {
// 1.定义的同时完全初始化
var arr1 [3]int = [3]int{1, 3, 5}
// 2.打印数组
fmt.Println(arr1) // [1 3 5]
// 1.定义的同时部分初始化
var arr4 [3]int = [3]int{8, 9}
// 2.打印数组
fmt.Println(arr4) // [8 9 0]
// 1.定义的同时指定元素初始化
var arr5 [3]int = [3]int{0:8, 2:9}
// 2.打印数组
fmt.Println(arr5) // [8 0 9]
// 1.先定义再逐个初始化
var arr3 [3]int
arr3[0] = 1
arr3[1] = 2
arr3[2] = 3
// 2.打印数组
fmt.Println(arr3) // [1 2 3]
}
+ 和C语言中的数组不同,Go语言中数组除了可以定义的同时初始化以外,还`可以先定义再一次性初始化`
package main
import "fmt"
func main() {
// 1.先定义再一次性初始化
var arr2 [3]int
arr2 = [3]int{2, 4, 6}
// 2.打印数组
fmt.Println(arr2) // [2 4 6]
}
+ 和C语言一样,Go语言中如果定义数组的同时初始化,那么元素个数可以省略,但是必须使用`...`来替代
- …会根据初始化元素个数自动确定数组长度
package main
import "fmt"
func main() {
// 1.定义的同时完全初始化
var arr1 = [...]int{1, 3, 5}
// 2.打印数组
fmt.Println(arr1) // [1 3 5]
// 1.定义的同时指定元素初始化
var arr2 = [...]int{6:5}
// 2.打印数组
fmt.Println(arr2) // [0 0 0 0 0 0 5]
}
- Go语言中数组的访问和使用方式和C语言一样都是通过
数组名称[索引]
的方式
package main
import "fmt"
func main() {
arr := [...]int{1, 3, 5}
// 使用数组, 往数组中存放数据
arr[1] = 666
// 访问数组, 从数组中获取数据
fmt.Println(arr[0])
fmt.Println(arr[1])
fmt.Println(arr[2])
}
- 遍历数组
- Go语言中提供了两种遍历数组的方式, 一种是通过传统for循环遍历, 一种是通过for…range循环遍历
package main
import "fmt"
func main() {
arr := [...]int{1, 3, 5}
// 传统for循环遍历
for i:=0; i<len(arr); i++{
fmt.Println(i, arr[i])
}
// for...range循环遍历
for i, v := range arr{
fmt.Println(i, v)
}
}
- 数组注意点
- Go语言中
数组长度
也是数据类型的一部分
- Go语言中
package main
import "fmt"
func main() {
var arr1 [3]int
var arr2 [3]int
//var arr3 [2]int
fmt.Println(arr1 == arr2) // true
//fmt.Println(arr1 == arr3) // 编译报错, 不是相同类型不能比较
}
+ 如果元素类型支持==、!=操作时,那么数组也支持此操作
package main
import "fmt"
func main() {
var arr1 [3]int = [3]int{1, 3, 5}
var arr2 [3]int = [3]int{1, 3, 5}
var arr3 [3]int = [3]int{2, 4, 6}
// 首先会判断`数据类型`是否相同,如果相同会依次取出数组中`对应索引的元素`进行比较,
// 如果所有元素都相同返回true,否则返回false
fmt.Println(arr1 == arr2) // true
fmt.Println(arr1 == arr3) // false
}
+ Go语言中的数组是值类型, 赋值和传参时会复制整个数组
package main
import "fmt"
func main() {
var arr1 [3]int = [3]int{1, 3, 5}
var arr2 [3]int = arr1
arr2[0] = 666
fmt.Println(arr1) // [1 3 5]
fmt.Println(arr2) // [666 3 5]
}
二维数组
- 用法和C语言数组一样, 只是创建的格式不同
- 格式:
[行数][列数]类型
package main
import "fmt"
func main() {
// 创建一个两行三列数组
arr := [2][3]int{
{1, 2, 3},
{4, 5, 6}, //注意: 数组换行需要以逗号结尾
}
fmt.Println(arr)// [[1 2 3] [4 5 6]]
}
- 创建多维数组时只允许第一维度使用…
- 格式:
[...][列数]类型
package main
import "fmt"
func main() {
// 创建一个两行三列数组
arr := [...][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(arr)// [[1 2 3] [4 5 6]]
}
切片
- 无论是C语言中的数组还是Go语言中的数组,数组的长度一旦确定就不能改变, 但在实际开发中我们可能事先不能确定数组的长度, 为了解决这类问题Go语言中推出了一种新的数据类型
切片
- 切片可以简单的理解为长度可以变化的数组, 但是Go语言中的切片本质上是一个结构体
- 切片源码
type slice struct{
array unsafe.Pointer // 指向底层数组指针
len int // 切片长度(保存了多少个元素)
cap int // 切片容量(可以保存多少个元素)
}
- 切片创建的三种方式
- 方式一: 通过数组创建切片
array[startIndex:endIndex]
package main
import "fmt"
func main() {
var arr = [5]int{1, 3, 5, 7, 9}
// 从数组0下标开始取,一直取到2下标前面一个索引
var sce = arr[0:2]
fmt.Println(sce) // [1 3]
// 切片len = 结束位置 - 开始位置
fmt.Println(len(sce)) // 2 - 0 = 2
fmt.Println(cap(sce)) // 5 - 0 = 5
// 数组地址就是数组首元素的地址
fmt.Printf("%p\n", &arr) // 0xc04200a330
fmt.Printf("%p\n", &arr[0]) // 0xc04200a330
// 切片地址就是数组中指定的开始元素的地址
// arr[0:2]开始地址为0, 所以就是arr[0]的地址
fmt.Printf("%p\n", sce) // 0xc04200a330
}
package main
import "fmt"
func main() {
var arr = [5]int{1, 3, 5, 7, 9}
// 根据数组的索引片段创建切片
var sce = arr[2:4]
fmt.Println(sce) // [5 7]
fmt.Println(len(sce)) // 4 - 2 = 2
fmt.Println(cap(sce)) // 5 - 2 = 3
fmt.Printf("%p\n", &arr[2]) // 0xc042076070
fmt.Printf("%p\n", sce) // 0xc042076070
}
- 指定起始位置时有三种方式可以指定
- 开始位置和结束位置都指定
- 只指定开始位置或结束位置
- 开始位置和结束位置都不指定
package main
import "fmt"
func main() {
var arr = [5]int{1, 3, 5, 7, 9}
// 同时指定开始位置和结束位置
var sce1 = arr[0:2]
fmt.Println(sce1) // [1 3]
// 只指定结束位置
var sce3 = arr[:2]
fmt.Println(sce3) // [1 3]
// 只指定开始位置
var sce2 = arr[0:]
fmt.Println(sce2) // [1 3 5 7 9]
// 都不指定
var sce4 = arr[:]
fmt.Println(sce4) // [1 3 5 7 9]
}
- 方式二: 通过make函数创建
make(类型, 长度, 容量)
- 内部会先创建一个数组, 然后让切片指向数组
- 如果没有指定容量,那么容量和长度一样
package main
import "fmt"
func main() {
// 第一个参数: 指定切片数据类型
// 第二个参数: 指定切片的长度
// 第三个参数: 指定切片的容量
var sce = make([]int, 3, 5)
fmt.Println(sce) // [0 0 0]
fmt.Println(len(sce)) // 3
fmt.Println(cap(sce)) // 5
/\*
内部实现原理
var arr = [5]int{0, 0, 0}
var sce = arr[0:3]
\*/
}
- 方式三:通过Go提供的语法糖快速创建
- 和创建数组一模一样, 但是
不能指定长度
- 通过该方式创建时, 切片的
长度和容量相等
- 和创建数组一模一样, 但是
package main
import "fmt"
func main() {
var sce = []int{1, 3, 5}
fmt.Println(sce) // [1 3 5]
fmt.Println(len(sce)) // 3
fmt.Println(cap(sce)) // 3
}
- 切片的使用
- 切片的基本使用方式和数组一样, 可以通过
切片名称[索引]
方式操作切片
- 切片的基本使用方式和数组一样, 可以通过
package main
import "fmt"
func main() {
var sce = []int{1, 3, 5}
// 使用切片, 往切片中存放数据
sce[1] = 666
// 访问切片, 从切片中获取数据
fmt.Println(sce) // [1 666 5]
}
+ 和数组一样, 如果通过`切片名称[索引]`方式操作切片, 不能越界
package main
import "fmt"
func main() {
var sce = []int{1, 3, 5}
// 编译报错, 越界
sce[3] = 666
}
+ 如果希望切片自动扩容,那么添加数据时必须使用append方法
- append函数会在切片`末尾`添加一个元素, 并返回一个追加数据之后的切片
- 利用append函数追加数据时,如果追加之后没有超出切片的容量,那么返回原来的切片, 如果追加之后超出了切片的容量,那么返回一个新的切片
- append函数每次给切片扩容都会按照原有切片容量\*2的方式扩容
package main
import "fmt"
func main() {
var sce = []int{1, 3, 5}
fmt.Println("追加数据前:", sce) // [1 3 5]
fmt.Println("追加数据前:", len(sce)) // 3
fmt.Println("追加数据前:", cap(sce)) // 3
fmt.Printf("追加数据前: %p\n", sce) // 0xc0420600a0
// 第一个参数: 需要把数据追加到哪个切片中
// 第二个参数: 需要追加的数据, 可以是一个或多个
sce = append(sce, 666)
fmt.Println("追加数据后:", sce) // [1 3 5 666]
fmt.Println("追加数据后:", len(sce)) // 4
fmt.Println("追加数据后:", cap(sce)) // 6
fmt.Printf("追加数据前: %p\n", sce) // 0xc042076b60
}
+ 除了append函数外,Go语言还提供了一个copy函数, 用于两个切片之间数据的快速拷贝
- 格式: `copy(目标切片, 源切片)`, 会将源切片中数据拷贝到目标切片中
package main
import "fmt"
func main() {
var sce1 = []int{1, 3, 5}
var sce2 = make([]int, 5)
fmt.Printf("赋值前:%p\n", sce1) // 0xc0420600a0
fmt.Printf("赋值前:%p\n", sce2) // 0xc042076060
// 将sce2的指向修改为sce1, 此时sce1和sce2底层指向同一个数组
sce2 = sce1
fmt.Printf("赋值后:%p\n", sce1) // 0xc0420600a0
fmt.Printf("赋值后:%p\n", sce2) // 0xc0420600a0
//copy(sce2, sce1)
fmt.Println(sce1) // [1 3 5]
fmt.Println(sce2) // [1 3 5]
sce2[1] = 666
fmt.Println(sce1) // [1 666 5]
fmt.Println(sce2) // [1 666 5]
}
package main
import "fmt"
func main() {
var sce1 = []int{1, 3, 5}
var sce2 = make([]int, 5)
fmt.Printf("赋值前:%p\n", sce1) // 0xc0420600a0
fmt.Printf("赋值前:%p\n", sce2) // 0xc042076060
// 将sce1中的数据拷贝到sce2中,, 此时sce1和sce2底层指向不同数组
copy(sce2, sce1)
fmt.Printf("赋值后:%p\n", sce1) // 0xc0420600a0
fmt.Printf("赋值后:%p\n", sce2) // 0xc042076060
//copy(sce2, sce1)
fmt.Println(sce1) // [1 3 5]
fmt.Println(sce2) // [1 3 5 0 0]
sce2[1] = 666
fmt.Println(sce1) // [1 3 5]
fmt.Println(sce2) // [1 666 5 0 0]
}
+ copy函数在拷贝数据时永远以小容量为准
package main
import "fmt"
func main() {
// 容量为3
var sce1 = []int{1, 3, 5}
// 容量为5
var sce2 = make([]int, 5)
fmt.Println("拷贝前:", sce2) // [0 0 0 0 0]
// sce2容量足够, 会将sce1所有内容拷贝到sce2
copy(sce2, sce1)
fmt.Println("拷贝后:", sce2) // [1 3 5 0 0]
}
package main
import "fmt"
func main() {
// 容量为3
var sce1 = []int{1, 3, 5}
// 容量为2
var sce2 = make([]int, 2)
fmt.Println("拷贝前:", sce2) // [0 0]
// sce2容量不够, 会将sce1前2个元素拷贝到sce2中
copy(sce2, sce1)
fmt.Println("拷贝后:", sce2) // [1 3]
}
- 切片的注意点
- 可以通过切片再次生成新的切片, 两个切片底层指向同一数组
package main
import "fmt"
func main() {
arr := [5]int{1, 3, 5, 7, 9}
sce1 := arr[0:4]
sce2 := sce1[0:3]
fmt.Println(sce1) // [1 3 5 7]
fmt.Println(sce2) // [1 3 5]
// 由于底层指向同一数组, 所以修改sce2会影响sce1
sce2[1] = 666
fmt.Println(sce1) // [1 666 5 7]
fmt.Println(sce2) // [1 666 5]
}
+ 和数组不同, 切片只支持判断是否为nil, 不支持==、!=判断
package main
import "fmt"
func main() {
var arr1 [3]int = [3]int{1, 3, 5}
var arr2 [3]int = [3]int{1, 3, 5}
var arr3 [3]int = [3]int{2, 4, 6}
// 首先会判断`数据类型`是否相同,如果相同会依次取出数组中`对应索引的元素`进行比较,
// 如果所有元素都相同返回true,否则返回false
fmt.Println(arr1 == arr2) // true
fmt.Println(arr1 == arr3) // false
sce1 := []int{1, 3, 5}
sce2 := []int{1, 3, 5}
//fmt.Println(sce1 == sce2) // 编译报错
fmt.Println(sce1 != nil) // true
fmt.Println(sce2 == nil) // false
}
+ 只声明当没有被创建的切片是不能使用的
package main
import "fmt"
func main() {
// 数组声明后就可以直接使用, 声明时就会开辟存储空间
var arr [3]int
arr[0] = 2
arr[1] = 4
arr[2] = 6
fmt.Println(arr) // [2 4 6]
// 切片声明后不能直接使用, 只有通过make或语法糖创建之后才会开辟空间,才能使用
var sce []int
sce[0] = 2 // 编译报错
sce[1] = 4
sce[2] = 6
fmt.Println(sce)
}
+ 字符串的底层是[]byte数组, 所以字符也支持切片相关操作
package main
import "fmt"
func main() {
str := "abcdefg"
// 通过字符串生成切片
sce1 := str[3:]
fmt.Println(sce1) // defg
sce2 := make([]byte, 10)
// 第二个参数只能是slice或者是数组
// 将字符串拷贝到切片中
copy(sce2, str)
fmt.Println(sce2) //[97 98 99 100 101 102 103 0 0 0]
}
map(字典、映射)
- map翻译过来就是字典或者映射, 可以把map看做是切片的升级版
- 切片是用来存储一组相同类型的数据的, map也是用来存储一组相同类型的数据的
- 在切片中我们可以通过索引获取对应的元素, 在map中我们可以通过key获取对应的元素
- 切片的索引是系统自动生成的,从0开始递增. map中的key需要我们自己指定
- 只要是可以做==、!=判断的数据类型都可以作为key(数值类型、字符串、数组、指针、结构体、接口)
- map的key的数据类型不能是:slice、map、function
- map和切片一样容量都不是固定的, 当容量不足时底层会自动扩容
- map格式:
var dic map[key数据类型]value数据类型
package main
import "fmt"
func main() {
var dic map[int]int = map[int]int{0:1, 1:3, 2:5}
fmt.Println(dic) // map[0:1 1:3 2:5]
// 获取map中某个key对应的值
fmt.Println(dic[0]) // 1
// 修改map中某个key对应的值
dic[1] = 666
fmt.Println(dic) // map[0:1 1:666 2:5]
}
package main
import "fmt"
func main() {
var dict map[string]string = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
fmt.Println(dict)// map[name:lnj age:33 gender:male]
}
- 创建map的三种方式
- 方式一: 通过Go提供的语法糖快速创建
package main
import "fmt"
func main() {
dict := map[string]string{"name":"lnj", "age":"33", "gender":"male"}
fmt.Println(dict)// map[name:lnj age:33 gender:male]
}
- 方式二:通过make函数创建
make(类型, 容量)
package main
import "fmt"
func main() {
var dict = make(map[string]string, 3)
dict["name"] = "lnj"
dict["age"] = "33"
dict["gender"] = "male"
fmt.Println(dict)// map[age:33 gender:male name:lnj]
}
- 方式二:通过make函数创建
make(类型)
package main
import "fmt"
func main() {
var dict = make(map[string]string)
dict["name"] = "lnj"
dict["age"] = "33"
dict["gender"] = "male"
fmt.Println(dict)// map[age:33 gender:male name:lnj]
}
- 和切片一样,没有被创建的map是不能使用的
package main
import "fmt"
func main() {
// map声明后不能直接使用, 只有通过make或语法糖创建之后才会开辟空间,才能使用
var dict map[string]string
dict["name"] = "lnj" // 编译报错
dict["age"] = "33"
dict["gender"] = "male"
fmt.Println(dict)
}
- map的增删改查
- 增加: 当map中没有指定的key时就会自动增加
package main
import "fmt"
func main() {
var dict = make(map[string]string)
fmt.Println("增加前:", dict) // map[]
dict["name"] = "lnj"
fmt.Println("增加后:", dict) // map[name:lnj]
}
- 修改: 当map中有指定的key时就会自动修改
package main
import "fmt"
func main() {
var dict = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
fmt.Println("修改前:", dict) // map[name:lnj age:33 gender:male]
dict["name"] = "zs"
fmt.Println("修改后:", dict) // map[age:33 gender:male name:zs]
}
- 删除: 可以通过Go语言内置delete函数删除指定元素
package main
import "fmt"
func main() {
var dict = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
fmt.Println("删除前:", dict) // map[name:lnj age:33 gender:male]
// 第一个参数: 被操作的字典
// 第二个参数: 需要删除元素对应的key
delete(dict, "name")
fmt.Println("删除后:", dict) // map[age:33 gender:male]
}
- 查询: 通过ok-idiom模式判断指定键值对是否存储
package main
import "fmt"
func main() {
var dict = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
//value, ok := dict["age"]
//if(ok){
// fmt.Println("有age这个key,值为", value)
//}else{
// fmt.Println("没有age这个key,值为", value)
//}
if value, ok := dict["age"]; ok{
fmt.Println("有age这个key,值为", value)
}
}
```go
- \*\*\*map遍历\*\*\*
+ 注意: map和数组以及切片不同,map中存储的数据是无序的, 所以多次打印输出的顺序可能不同
```go
var dict = map[string]string{"name":"lnj", "age":"33", "gender":"male"}
for key, value := range dict{
fmt.Println(key, value)
}
结构体
- Go语言中的结构体和C语言中结构体一样, 都是用来保存一组
不同类型的数据
- Go语言中的结构体和C语言中结构体一样, 都需要先定义结构体类型再利用结构体类型定义结构体变量
- 定义结构体类型
type 类型名称 struct{
属性名称 属性类型
属性名称 属性类型
... ...
}
type Studentstruct {
name string
age int
}
- 创建结构体变量的两种方式
- 方式一: 先定义结构体类型, 再定义结构体变量
- 和C语言中的结构体一样, 如果结构体类型需要多次使用, 那么建议先定义类型再定义变量
- 方式一: 先定义结构体类型, 再定义结构体变量
package main
import "fmt"
func main() {
type Student struct {
name string
age int
}
// 完全初始化
var stu1= Student{"lnj", 33}
fmt.Println(stu1)
// 部分初始化
// 部分初始化必须通过 属性名称: 方式指定要初始化的属性
var stu2 = Student{name:"lnj"}
fmt.Println(stu2)
}
+ 方式二: 定义结构体类型同时定义结构体变量(匿名结构体)
- 和C语言中的结构体一样, 如果结构体类型只需要使用一次, 那么建议定义类型同时定义变量
package main
import "fmt"
func main() {
// 注意: 这里不用写type和结构体类型名称
var stu2 = struct {
name string
age int
}{
name: "lnj",
age: 33,
}
fmt.Println(stu2)
}
- 结构体类型操作
package main
import "fmt"
type Student struct {
name string
age int
}
func main() {
var stu= Student{"lnj", 33}
// 获取结构体中某个属性对应的值
fmt.Println(stu.name)
// 修改结构体中某个属性对应的值
stu.name = "zs"
fmt.Println(stu)
}
- 和slice、map不同的是, 只要定义了结构体变量就可以使用结构体变量
- 默认情况下结构体中的所有属性都是属性对应类型的默认值
package main
import "fmt"
type Student struct {
name string
age int
}
func main() {
var stu Student // 相当于 var stu = Student{}
fmt.Println(stu) // { 0}
stu.name = "lnj" // 不会报错
stu.age = 33
fmt.Println(stu) // {lnj 33}
}
- 复杂结构体成员
- 创建时可以按照属性单独存在时初始化方式初始化
package main
import "fmt"
type Student struct {
name string
age int
}
func main() {
type Demo struct {
age int // 基本类型作为属性
arr [3]int // 数组类型作为属性
sce []int // 切片类型作为属性
mp map[string]string // 字典类型作为属性
stu Student // 结构体类型作为属性
}
var d Demo = Demo{
33,
[3]int{1, 3, 5},
[]int{2, 4, 6},
map[string]string{"class":"one"},
Student{
"lnj",
33,
},
}
fmt.Println(d) // {33 [1 3 5] [2 4 6] map[class:one] {lnj 33}}
}
+ slice、map类型属性默认值是nil,不能直接使用
package main
import "fmt"
type Student struct {
name string
age int
}
func main() {
type Demo struct {
age int // 基本类型作为属性
arr [3]int // 数组类型作为属性
sce []int // 切片类型作为属性
mp map[string]string // 字典类型作为属性
stu Student // 结构体类型作为属性
}
// 定义结构体变量
var d Demo
// 可以直接使用基本类型属性
d.age = 33
// 可以直接使用数组类型属性
d.arr[0] = 666
// 不可以直接使用切片类型属性
//d.sce[0] = 888 // 编译报错
d.sce = make([]int, 5) // 先创建
d.sce[0] = 888 // 后使用
// 不可以直接使用字典类型属性
//d.mp["class"] = "one" // 编译报错
d.mp = make(map[string]string)// 先创建
d.mp["class"] = "one"// 后使用
// 可以直接使用结构体类型属性
d.stu.name = "lnj"
fmt.Println(d) // {33 [666 0 0] [888 0 0 0 0] map[class:one] {lnj 0}}
}
- 结构体类型转换
- 属性名、属性类型、属性个数、排列顺序都是类型组成部分
- 只有属性名、属性类型、属性个数、排列顺序都相同的结构体类型才能转换
package main
import "fmt"
func main() {
type Person1 struct {
name string
age int
}
type Person2 struct {
name string
age int
}
type Person3 struct {
age int
name string
}
type Person4 struct {
nm string
age int
}
type Person5 struct {
name string
age string
}
type Person6 struct {
age int
name string
gender string
}
var p1 Person1 = Person1{"lnj", 33}
var p2 Person2
// 类型名称不一样不能直接赋值(Person1、Person2)
//p2 = p1
// 虽然类型名称不一样, 但是两个类型中的`属性名称`、`属性类型`、`属性个数`、`排列顺序`都一样,所以可以强制转换
p2 = Person2(p1)
fmt.Println(p2)
// 两个结构体类型中的`属性名称`、`属性类型`、`属性个数`都一样,但是`排列顺序`不一样,所以不能强制转换
//var p3 Person3
//p3 = Person3(p1)
//fmt.Println(p3)
// 两个结构体类型中的`属性类型`、`属性个数`、`排列顺序`都一样,但是`属性名称`不一样,所以不能强制转换
//var p4 Person4
//p4 = Person4(p1)
//fmt.Println(p4)
// 两个结构体类型中的`属性名称`、`属性个数`、`排列顺序`都一样,但是`属性类型`不一样,所以不能强制转换
//var p5 Person5
//p5 = Person5(p1)
//fmt.Println(p5)
// 两个结构体类型中的`属性名称`、`属性类型`、`排列顺序`都一样,但是`属性个数`不一样,所以不能强制转换
//var p6 Person6
//p6 = Person6(p1)
//fmt.Println(p6)
}
- 匿名属性
- 没有指定属性名称,只有属性的类型, 我们称之为匿名属性
- 任何
有命名的数据类型
都可以作为匿名属性(int、float、bool、string、struct等)
package main
import "fmt"
func main() {
type Person struct {
int
float32
bool
string
}
// 不指定名称初始化
per1 := Person{3, 3.14, false, "lnj"}
fmt.Println(per1)
// 可以把数据类型作为名字显示初始化
per2 := Person{
int: 3,
float32: 3.14,
bool: true,
string: "lnj",
}
fmt.Println(per2)
// 可以把数据类型当做属性名称操作结构体
per2.int = 666
fmt.Println(per2.int) // 666
}
+ Go语言中最常见的匿名属性是用`结构体类型作为匿名属性`
package main
import "fmt"
func main() {
type Person struct {
name string
age int
}
type Student struct {
Person // 匿名属性
class string
}
stu := Student{
Person{"lnj", 33},
"学前一班",
}
fmt.Println(stu) // {{lnj 33} 学前一班}
}
+ 如果结构体作为匿名属性, 想访问匿名属性的属性有两种方式
package main
import "fmt"
func main() {
type Person struct {
name string
age int
}
type Student struct {
Person // 匿名属性
class string
}
stu := Student{
Person{"lnj", 33},
"学前一班",
}
fmt.Println(stu) // {{lnj 33} 学前一班}
// 方式一: 先找到匿名属性,再访问匿名属性中的属性
stu.Person.name = "zs"
fmt.Println(stu) // {{zs 33} 学前一班}
// 方式二: 直接访问匿名属性中的属性
// 系统会先查找当前结构体有没有名称叫做name的属性
// 如果没有会继续查找匿名属性中有没有名称叫做name的属性
stu.name = "ww"
fmt.Println(stu) // {{ww 33} 学前一班}
}
+ 注意点: 如果多个匿名属性的属性名称相同,那么不能通过方式二操作,只能通过方式一
package main
import "fmt"
func main() {
type Person struct {
name string
age int
}
type Class struct {
name string
time string
}
type Student struct {
Person // 匿名属性
Class // 匿名属性
}
stu := Student{
Person{"lnj", 33},
Class{"学前一班", "2020-12-12"},
}
fmt.Println(stu) // {{lnj 33} {学前一班 2020-12-12}}
// 编译报错, 系统搞不清楚要找哪个name
//stu.name = "zs"
stu.Person.name = "zs"
stu.Class.name = "小学一年级"
fmt.Println(stu) // {{zs 33} {小学一年级 2020-12-12}}
}
+ 注意点: 只有匿名结构体才支持向上查找
package main
import "fmt"
func main() {
type Person struct {
name string
}
type Student struct {
per Person
age int
}
var stu Student = Student{Person{"lnj"}, 18}
//fmt.Println(stu.name) // 报错
fmt.Println(stu.per.name) // 必须通过属性进一步查找
fmt.Println(stu.age)
}
+ 注意点: 如果匿名属性是一个结构体类型, 那么这个结构体类型不能是自己
package main
import "fmt"
func main() {
type Person struct {
Person // 错误
name string
}
type Student struct {
\*Student // 正确, 链表
age int
}
}
普通指针
- 和C语言一样, 允许用一个变量来存放其它变量的地址, 这种专门用于存储其它变量地址的变量, 我们称之为指针变量
- 和C语言一样, Go语言中的指针无论是什么类型占用内存都一样(32位4个字节, 64位8个字节)
package main
import (
"fmt"
"unsafe"
)
func main() {
var p1 \*int;
var p2 \*float64;
var p3 \*bool;
fmt.Println(unsafe.Sizeof(p1)) // 8
fmt.Println(unsafe.Sizeof(p2)) // 8
fmt.Println(unsafe.Sizeof(p3)) // 8
}
- 和C语言一样, 只要一个指针变量保存了另一个变量对应的内存地址, 那么就可以通过*来访问指针变量指向的存储空间
package main
import (
"fmt"
)
func main() {
// 1.定义一个普通变量
var num int = 666
// 2.定义一个指针变量
var p \*int = &num
fmt.Printf("%p\n", &num) // 0xc042064080
fmt.Printf("%p\n", p) // 0xc042064080
fmt.Printf("%T\n", p) // \*int
// 3.通过指针变量操作指向的存储空间
\*p = 888
// 4.指针变量操作的就是指向变量的存储空间
fmt.Println(num) // 888
fmt.Println(\*p) // 888
}
指向数组指针
- 在C语言中, 数组名,&数组名,&数组首元素保存的都是同一个地址
#include <stdio.h>
int main(){
int arr[3] = {1, 3, 5};
printf("%p\n", arr); // 0060FEA4
printf("%p\n", &arr); // 0060FEA4
printf("%p\n", &arr[0]); // 0060FEA4
}
- 在Go语言中通过数组名无法直接获取数组的内存地址
package main
import "fmt"
func main() {
var arr [3]int = [3]int{1, 3, 5}
fmt.Printf("%p\n", arr) // 乱七八糟东西
fmt.Printf("%p\n", &arr) // 0xc0420620a0
fmt.Printf("%p\n", &arr[0]) // 0xc0420620a0
}
- 在C语言中, 无论我们将数组名,&数组名,&数组首元素赋值给指针变量, 都代表指针变量指向了这个数组
#include <stdio.h>
int main(){
int arr[3] = {1, 3, 5};
int \*p1 = arr;
p1[1] = 6;
printf("%d\n", arr[1]);
int \*p2 = &arr;
p2[1] = 7;
printf("%d\n", arr[1]);
int \*p3 = &arr[0];
p3[1] = 8;
printf("%d\n", arr[1]);
}
- 在Go语言中, 因为只有数据类型一模一样才能赋值, 所以只能通过&数组名赋值给指针变量, 才代表指针变量指向了这个数组
package main
import "fmt"
func main() {
// 1.错误, 在Go语言中必须类型一模一样才能赋值
// arr类型是[3]int, p1的类型是\*[3]int
var p1 \*[3]int
fmt.Printf("%T\n", arr)
fmt.Printf("%T\n", p1)
p1 = arr // 报错
p1[1] = 6
fmt.Println(arr[1])
// 2.正确, &arr的类型是\*[3]int, p2的类型也是\*[3]int
var p2 \*[3]int
fmt.Printf("%T\n", &arr)
fmt.Printf("%T\n", p2)
p2 = &arr
p2[1] = 6
fmt.Println(arr[1])
// 3.错误, &arr[0]的类型是\*int, p3的类型也是\*[3]int
var p3 \*[3]int
fmt.Printf("%T\n", &arr[0])
fmt.Printf("%T\n", p3)
p3 = &arr[0] // 报错
p3[1] = 6
fmt.Println(arr[1])
}
- 注意点:
- Go语言中的指针, 不支持C语言中的+1 -1和++ – 操作
package main
import "fmt"
func main() {
var arr [3]int = [3]int{1, 3, 5}
var p \*[3]int
p = &arr
fmt.Printf("%p\n", &arr) // 0xc0420620a0
fmt.Printf("%p\n", p) // 0xc0420620a0
fmt.Println(&arr) // &[1 3 5]
fmt.Println(p) // &[1 3 5]
// 指针指向数组之后操作数组的几种方式
// 1.直接通过数组名操作
arr[1] = 6
fmt.Println(arr[1])
// 2.通过指针间接操作
(\*p)[1] = 7
fmt.Println((\*p)[1])
fmt.Println(arr[1])
// 3.通过指针间接操作
p[1] = 8
fmt.Println(p[1])
fmt.Println(arr[1])
// 注意点: Go语言中的指针, 不支持+1 -1和++ --操作
\*(p + 1) = 9 // 报错
fmt.Println(\*p++) // 报错
fmt.Println(arr[1])
}
指向切片的指针
- 值得注意点的是切片的本质就是一个指针指向数组, 所以指向切片的指针是一个二级指针
package main
import "fmt"
func main() {
// 1.定义一个切片
var sce[]int = []int{1, 3, 5}
// 2.打印切片的地址
// 切片变量中保存的地址, 也就是指向的那个数组的地址 sce = 0xc0420620a0
fmt.Printf("sce = %p\n",sce )
fmt.Println(sce) // [1 3 5]
// 切片变量自己的地址, &sce = 0xc04205e3e0
fmt.Printf("&sce = %p\n",&sce )
fmt.Println(&sce) // &[1 3 5]
// 3.定义一个指向切片的指针
var p \*[]int
// 因为必须类型一致才能赋值, 所以将切片变量自己的地址给了指针
p = &sce
// 4.打印指针保存的地址
// 直接打印p打印出来的是保存的切片变量的地址 p = 0xc04205e3e0
fmt.Printf("p = %p\n", p)
fmt.Println(p) // &[1 3 5]
// 打印\*p打印出来的是切片变量保存的地址, 也就是数组的地址 \*p = 0xc0420620a0
fmt.Printf("\*p = %p\n", \*p)
fmt.Println(\*p) // [1 3 5]
// 5.修改切片的值
// 通过\*p找到切片变量指向的存储空间(数组), 然后修改数组中保存的数据
(\*p)[1] = 666
fmt.Println(sce[1])
}
指向字典指针
- 与普通指针并无差异
package main
import "fmt"
func main() {
var dict map[string]string = map[string]string{"name":"lnj", "age":"33"}
var p \*map[string]string = &dict
(\*p)["name"] = "zs"
fmt.Println(dict)
}
指向结构体指针
- Go语言中指向结构体的指针和C语言一样
- 结构体和指针
- 创建结构体指针变量有两种方式
package main
import "fmt"
type Student struct {
name string
age int
}
func main() {
// 创建时利用取地址符号获取结构体变量地址
var p1 = &Student{"lnj", 33}
fmt.Println(p1) // &{lnj 33}
// 通过new内置函数传入数据类型创建
// 内部会创建一个空的结构体变量, 然后返回这个结构体变量的地址
var p2 = new(Student)
fmt.Println(p2) // &{ 0}
}
+ 利用结构体指针操作结构体属性
package main
import "fmt"
type Student struct {
name string
age int
}
func main() {
var p = &Student{}
// 方式一: 传统方式操作
// 修改结构体中某个属性对应的值
// 注意: 由于.运算符优先级比\*高, 所以一定要加上()
(\*p).name = "lnj"
// 获取结构体中某个属性对应的值
fmt.Println((\*p).name) // lnj
// 方式二: 通过Go语法糖操作
// Go语言作者为了程序员使用起来更加方便, 在操作指向结构体的指针时可以像操作接头体变量一样通过.来操作
// 编译时底层会自动转发为(\*p).age方式
p.age = 33
fmt.Println(p.age) // 33
}
指针作为函数参数和返回值
- 如果指针类型作为函数参数, 修改形参会影响实参
- 不能将函数内的指向局部变量的指针作为返回值, 函数结束指向空间会被释放
- 可以将函数内的局部变量作为返回值, 本质是拷贝一份
方法
-
Go语言中的方法其实就是一个特殊函数, 只不过这个函数是和某种属性类型绑定在一起的而已
-
Go语言中的方法
一般用于
将函数和结构体绑定在一起
, 让结构体除了能够保存数据外还能具备某些行为 -
将函数和数据类型绑定的格式
- 只需要在函数名称前面加上(接收者 数据类型), 即可将函数和某种数据类型绑定在一起
func (接收者 数据类型)方法名称(形参列表)(返回值列表){
方法体
}
- 示例: 给结构体添加一个方法,此时结构体除了可以保存数据,还具备说出自己名字和年龄的行为
package main
import "fmt"
// 1.定义一个结构体
type Person struct {
name string
age int
}
// 2.定义一个函数, 并将这个函数和Person结构体绑定在一起
func (p Person)say() {
fmt.Println("my name is", p.name, "my age is", p.age)
}
func main() {
// 3.创建一个结构体变量
per := Person{"lnj", 33}
// 4.利用结构体变量调用和结构体绑定的方法
// 调用时会自动将调用者(per)传递给方法的接收者(p)
// 所以可以在方法内部通过p方法结构体变量的属性
per.say()
}
- 方法和函数异同
- 方法的数据类型也是函数类型, 所以也可以定义变量保存(作为参数返回值等)
package main
import "fmt"
type Person struct {
name string
age int
}
// 定义一个方法
func (p Person)say() {
fmt.Println("say方法")
}
// 定义一个函数
func say() {
fmt.Println("say函数")
}
func main() {
p := Person{"lnj", 33}
fmt.Printf("%T\n", p.say) // func()
fmt.Printf("%T\n", say) // func()
// 定义一个保存没有形参没有返回值的函数类型变量
var fn func()
// 利用函数类型变量保存一个方法
fn = p.say
// 利用函数类型变量调用一个方法
fn()
// 利用函数类型变量保存一个函数
fn = say
// 利用函数类型变量调用一个函数
fn()
}
+ 方法只能通过绑定类型的变量调用, 函数可以直接调用
package main
import "fmt"
type Person struct {
name string
age int
}
// 定义一个方法
func (p Person)say() {
fmt.Println("my name is", p.name, "my age is", p.age)
}
// 定义一个函数
func test() {
fmt.Println("test")
}
func main() {
per := Person{"lnj", 33}
per.say() // 方法只能通过绑定类型的变量调用
//say() // 编译报错, 不能直接调用
test() // 编译通过, 可以直接调用
}
+ 方法的接收者可以看做就是函数的一个形参
package main
import "fmt"
type Person struct {
name string
age int
}
// 定义一个方法
func (p Person)say() {
fmt.Println("my name is", p.name, "my age is", p.age)
}
// 定义一个函数
func test(p Person) {
fmt.Println("my name is", p.name, "my age is", p.age)
}
func main() {
per := Person{"lnj", 33}
per.say() // my name is lnj my age is 33
test(per) // my name is lnj my age is 33
}
+ 既然可以看做形参, 那么自然也具备形参的特点(值传递和地址传递)
package main
import "fmt"
type Person struct {
name string
age int
}
// 接收者是一个变量
func (p Person)setName(name string) {
p.name = name
}
// 接收者是一个指针
func (p \*Person)setAge(age int) {
p.age = age
}
func main() {
per := Person{"lnj", 33}
fmt.Println(per) // {lnj 33}
// 值传递, 方法内部修改不会影响方法外部
per.setName("zs")
fmt.Println(per) // {lnj 33}
p := &per
// 地址传递, 方法内部修改会影响方法外部
(\*p).setAge(18)
fmt.Println(per) // {lnj 18}
}
+ 地址传递的几种调用方式
package main
import "fmt"
type Person struct {
name string
age int
}
// 接收者是一个变量
func (p Person)setName(name string) {
p.name = name
}
// 接收者是一个指针
func (p \*Person)setAge(age int) {
p.age = age
}
func main() {
per := Person{"lnj", 33}
// 方式一: 先拿到指针,然后再通过指针调用
p := &per
(\*p).setAge(18)
fmt.Println(per) // {lnj 18}
// 方式二: 直接利用变量调用, 底层会自动获取变量地址传递给接收者
per.setAge(66)
fmt.Println(per) // {lnj 66}
}
接口
- Go语言中的接口和现实生活中的USB插槽很像, 它定义某种标准, 但不关心具体实现
- 无论你到哪个商店里面去购买USB线,只要你告诉商家你需要一根USB线, 买回家之后就一定能插到电脑上使用. 之所以能用,原因就是电脑厂商在指定了USB插槽的标准(尺寸、排线等等), 生产厂家只需要按照标准生产即可
- 同样在Go语言中我们可以通过接口来定义某种标准(函数声明),但不用不关心具体实现(函数实现), 只要将来有人按照标准实现了接口,我们就可以使用
- 定义接口格式
type 接口名称 interface{
函数声明
}
- 示例: 定义一个通用的USB接口
package main
import "fmt"
// 1.定义一个接口
type usber interface {
start()
stop()
}
type Computer struct {
name string
model string
}
// 2.实现接口中的所有方法
func (cm Computer)start() {
fmt.Println("启动电脑")
}
func (cm Computer)stop() {
fmt.Println("关闭电脑")
}
type Phone struct {
name string
model string
}
// 2.实现接口中的所有方法
func (p Phone)start() {
fmt.Println("启动手机")
}
func (p Phone)stop() {
fmt.Println("关闭手机")
}
// 3.使用接口定义的方法
func working(u usber) {
u.start()
u.stop()
}
func main() {
cm := Computer{"戴尔", "F1234"}
working(cm) // 启动电脑 关闭电脑
p := Phone{"华为", "M10"}
working(p) // 启动手机 关闭手机
}
- 接口注意点
- 接口中只能有方法的声明不能有方法的实现
type usber interface {
func start(){ // 错误
fmt.Println("启动")
}
func stop() { // 错误
fmt.Println("停止")
}
}
+ 接口中只能有方法什么不能有字段
type usber interface {
name string // 错误
start()
stop()
}
+ 只有实现了接口中所有的方法, 才算实现了接口, 才能用`该接口类型`接收
package main
import "fmt"
// 1.定义一个接口
type usber interface {
start()
stop()
}
type Computer struct {
name string
model string
}
// 2.实现接口中的所有方法
func (cm Computer)start() {
fmt.Println("启动电脑")
}
func (cm Computer)stop() {
fmt.Println("关闭电脑")
}
// 2.只实现了接口中部分方法
type Phone struct {
name string
model string
}
func (p Phone)start() {
fmt.Println("启动手机")
}
func main() {
// 1.定义一个usber接口类型变量
var i usber
// 2.用usber接口类型变量接收Computer类型结构体
i = Computer{"戴尔", "F1234"} // 实现了所有方法, 不会报错
// 3.用usber接口类型变量接收Phone类型结构体
//i = Phone{"华为", "M10"} // 只实现了部分方法, 会报错
fmt.Println(i)
}
+ 和结构体一样,接口中也可以嵌入接口
package main
import "fmt"
type A interface {
fna()
}
type B interface {
fnb()
}
type C interface {
A // 嵌入A接口
B // 嵌入B接口
fnc()
}
type Person struct {}
func (p Person)fna() {
fmt.Println("实现A接口中的方法")
}
func (p Person)fnb() {
fmt.Println("实现B接口中的方法")
}
func (p Person)fnc() {
fmt.Println("实现C接口中的方法")
}
func main() {
p := Person{}
p.fna() // 实现A接口中的方法
p.fnb() // 实现B接口中的方法
p.fnc() // 实现C接口中的方法
}
+ 和结构体一样,接口中嵌入接口时不能嵌入自己
type A interface {
A // 报错, 不能自己搞自己
}
+ 接口中嵌入接口时不能出现相同的方法名称
type A interface {
fn()
}
type B interface {
fn()
}
type C interface {
A
B // 报错, A接口和B接口都有名称叫做fn的方法
fnc()
}
+ 超集接口变量可以自动转换成子集接口变量, 子集接口变量不能转换为超集接口变量(本质就是没有实现所有方法)
package main
import "fmt"
type aer interface {
fna()
}
type ber interface {
aer
fnb()
}
// Person实现了超集接口所有方法
type Person struct {}
func (p Person)fna() {
fmt.Println("实现A接口中的方法")
}
func (p Person)fnb() {
fmt.Println("实现B接口中的方法")
}
// Student实现了子集接口所有方法
type Student struct { }
func (p Student)fna() {
fmt.Println("实现A接口中的方法")
}
func main() {
var i ber
// 子集接口变量不能转换为超集接口变量
//i = Student{}
fmt.Println(i)
var j aer
// 超集接口变量可以自动转换成子集接口变量,
j = Person{}
fmt.Println(j)
}
+ 空接口类型可以接收任意类型数据
package main
import "fmt"
func main() {
// 1.定义一个空接口类型变量
var i interface{}
// 2.用接口类型保存任意类型数据
i = 123
fmt.Println(i) // 123
i = 3.14
fmt.Println(i) // 3.14
i = "lnj"
fmt.Println(i) // lnj
i = [3]int{1, 3, 5}
fmt.Println(i) // [1 3 5]
i = []int{2, 4, 6}
fmt.Println(i) // [2 4 6]
i = map[string]string{"name": "lnj"}
fmt.Println(i) // map[name:lnj]
i = Computer{"戴尔", "F1234"}
fmt.Println(i) // {戴尔 F1234}
}
+ 只要是自定义类型就可以实现接口
package main
import "fmt"
// 1.定义一个接口
type usber interface {
start()
stop()
}
// 2.自定义int类型
type integer int
// 2.实现接口中的所有方法
func (i integer)start() {
fmt.Println("int类型实现接口")
}
func (i integer)stop() {
fmt.Println("int类型实现接口")
}
func main() {
var i integer = 666
i.start() // int类型实现接口
i.stop() // int类型实现接口
}
- 接口类型转换
- 接口类型变量可以接收实现了该接口类型的变量, 但是只能调用该变量中的方法, 不能访问该变量的属性
package main
import "fmt"
type studier interface {
read()
}
type Person struct {
name string
age int
}
func (p Person)read() {
fmt.Println(p.name, "正在学习")
}
func main() {
// 1.定义一个接口类型变量
var s studier
// 2.用接口类型变量接收实现了接口的结构体
s = Person{"lnj", 33}
s.name = "zs" // 报错, 由于s是接口类型, 所以不能访问属性
fmt.Println(s)
}
+ 想要访问变量中的属性, 必须将接口类型还原为原始类型
package main
import "fmt"
type studier interface {
read()
}
type Person struct {
name string
age int
}
func (p Person)read() {
fmt.Println(p.name, "正在学习")
}
func main() {
var s studier
s = Person{"lnj", 33}
s.name = "zs" // 报错, 由于s是接口类型, 所以不能访问属性
// 2.定义一个结构体类型变量
//var p Person
// 不能用强制类型转换方式将接口类型转换为原始类型
//p = Person(s) // 报错
// 2.利用ok-idiom模式将接口类型还原为原始类型
// s.(Person)这种格式我们称之为: 类型断言
if p, ok := s.(Person); ok {
p.name = "zs"
fmt.Println(p)
}
// 2.通过 type switch将接口类型还原为原始类型
// 注意: type switch不支持fallthrought
switch p := s.(type) {
case Person:
p.name = "zs"
fmt.Println(p) // {zs 33}
default:
fmt.Println("不是Person类型")
}
}
+ 除了可以将接口类型转换为原始类型以外, 企业开发中有时候可能我们还需要将抽象接口类型转换为具体接口类型
package main
import "fmt"
type studier interface {
read()
}
type Person struct {
name string
age int
}
func (p Person)read() {
fmt.Println(p.name, "正在学习")
}
func main() {
// 1.定义一个抽象接口类型
var i interface{}
i = Person{"lnj", 33}
// 不能调用read方法, 因为抽象接口中没有这个方法
//i.read()
// 2.利用ok-idiom模式将抽象接口转换为具体接口
if s, ok := i.(studier); ok{
// 可以调用read方法,因为studier中声明了这个方法,并且结构体中实现了这个方法
s.read() // lnj 正在学习
}
}
面向对象基本概念
面向对象思想
- 面向对象(Object Oriented,OO)是软件开发方法
- 面向对象是一种对现实世界抽象的理解,是计算机编程技术发展到一定阶段后的产物
- Object Oriented Programming-OOP ——面向对象编程
面向对象和面向过程区别
-
面向对象是相对面向过程而言
-
面向对象和面向过程都是一种思想
-
面向过程
- 强调的是功能行为
- 关注的是解决问题需要哪些步骤
-
回想下前面我们完成一个需求的步骤:
- 首先搞清楚我们要做什么
- 然后分析怎么做
- 最后我用代码体现
- 一步一步去实现,而具体的每一步都需要我们去实现和操作
-
在上面每一个具体步骤中我们都是参与者, 并且需要面对具体的每一个步骤和过程, 这就是面向过程最直接的体现
- 面向对象
- 将功能封装进对象,强调具备了功能的对象
- 关注的是解决问题需要哪些对象
- 当需求单一, 或者简单时, 我们一步一步去操作没问题, 并且效率也挺高。 可随着需求的更改, 功能的增加, 发现需要面对每一个步骤非常麻烦, 这时就开始思索, 能不能把这些步骤和功能再进行封装, 封装时根据不同的功能,进行不同的封装,功能类似的封装在一起。这样结构就清晰多了, 用的时候, 找到对应的类就可以了, 这就是面向对象思想
- 示例
- 买电脑
-
面向过程
- 了解电脑
- 了解自己的需求
- 对比参数
- 去电脑城
- 砍价,付钱
- 买回电脑
- 被坑
-
面向对象
- 找班长
- 描述需求
- 班长把电脑买回来
-
- 吃饭
-
面向过程
- 去超市卖菜
- 摘菜
- 洗菜
- 切菜
- 炒菜
- 盛菜
- 吃
-
面向对象
- 去饭店
- 点菜
- 吃
-
- 洗衣服
- 面向过程
- 脱衣服
- 放进盆里
- 放洗衣液
- 加水
- 放衣服
- 搓一搓
- 清一清
- 拧一拧
- 晒起来
- 面向对象
- 脱衣服
- 打开洗衣机
- 丢进去
- 一键洗衣烘干
- 终极面向对象
- 买电脑/吃饭/洗衣服
- 找个对象
- 面向过程
- 现实生活中我们是如何应用面相对象思想的
- 包工头
- 汽车坏了
- 面试
面向对象的特点
- 是一种符合人们思考习惯的思想
- 可以将复杂的事情简单化
- 将程序员从执行者转换成了指挥者
- 完成需求时:
- 先要去找具有所需的功能的对象来用
- 如果该对象不存在,那么创建一个具有所需功能的对象
- 这样简化开发并提高复用
类与对象的关系
-
面向对象的核心就是对象,那怎么创建对象?
- 现实生活中可以根据模板创建对象,编程语言也一样,也必须先有一个模板,在这个模板中说清楚将来创建出来的对象有哪些
属性
和行为
- 现实生活中可以根据模板创建对象,编程语言也一样,也必须先有一个模板,在这个模板中说清楚将来创建出来的对象有哪些
-
Go语言中的类相当于图纸,用来描述一类事物。也就是说要想创建对象必须先有类
-
Go语言利用类来创建对象,对象是类的具体存在, 因此面向对象解决问题应该是先考虑需要设计哪些类,再利用类创建多少个对象
如何设计一个类
- 生活中描述事物无非就是描述事物的
属性
和行为
。- 如:人有身高,体重等属性,有说话,打架等行为。
事物名称(类名):人(Person)
属性:身高(height)、年龄(age)
行为(功能):跑(run)、打架(fight)
- Go语言中用类来描述事物也是如此
- 属性:对应类中的成员变量。
- 行为:对应类中的成员方法。
- 定义类其实在定义类中的成员(成员变量和成员方法)
- 拥有相同或者类似
属性
(状态特征)和行为
(能干什么事)的对象都可以抽像成为一个类
如何分析一个类
- 一般名词都是类(名词提炼法)
- 飞机发射两颗炮弹摧毁了8辆装甲车
飞机
炮弹
装甲车
- 隔壁老王在公车上牵着一条叼着热狗的草泥马
老王
热狗
草泥马
如何定义一个类
- 类是用于描述事物的的属性和行为的, 而Go语言中的结构体正好可以用于描述事物的属性和行为
- 所以在Go语言中我们使用结构体来定义一个类型
type Person struct {
name string // 人的属性
age int // 人的属性
}
// 人的行为
func (p Person)Say() {
fmt.Println("my name is", p.name, "my age is", p.age)
}
如何通过类创建一个对象
- 不过就是创建结构体的时候, 根据每个对象的特征赋值不同的属性罢了
// 3.创建一个结构体变量
p1 := Person{"lnj", 33}
per.say()
p2 := Person{"zs", 18}
per.Say()
不同包中变量、函数、方法、类型公私有问题
- 在Go语言中通过首字母大小写来控制变量、函数、方法、类型的公私有
- 如果首字母小写, 那么代表私有, 仅在当前包中可以使用
- 如果首字母大写, 那么代表共有, 其它包中也可以使用
package demo
import "fmt"
var num1 int = 123 // 当前包可用
var Num1 int = 123 // 其它包也可用
type person struct { // 当前包可用
name string // 当前包可用
age int // 当前包可用
}
type Student struct { // 其它包也可用
Name string // 其它包也可用
Age int // 其它包也可用
}
func test1() { // 当前包可用
fmt.Println("test1")
}
func Test2() { // 其它包也可用
fmt.Println("Test2")
}
func (p person)say() { // 当前包可用
fmt.Println(p.name, p.age)
}
func (s Student)Say() { // 其它包也可用
fmt.Println(s.Name, s.Age)
}
面向对象三大特性
- 封装性
- 封装性就是隐藏实现细节,仅对外公开接口
- 类是数据与功能的封装,数据就是成员变量,功能就是方法
- 封装性就是隐藏实现细节,仅对外公开接口
- 为什么要封装?
- 不封装的缺点:当一个类把自己的成员变量暴露给外部的时候,那么该类就失去对该成员变量的管理权,别人可以任意的修改你的成员变量
- 封装就是将数据隐藏起来,只能用此类的方法才可以读取或者设置数据,不可被外部任意修改是面向对象设计本质(
将变化隔离
)。这样降低了数据被误用的可能 (提高安全性
和灵活性
)
package model
import "fmt"
type Person struct { // 其它包也可用
name string // 当前包可用
age int // 当前包可用
}
func (p \*person)SetAge(age int) {
// 安全校验
if age < 0 {
fmt.Println("年龄不能为负数")
}
p.age = age
}
package main
import (
"fmt"
"main/model"
)
func main() {
// 报错, 因为name和age不是公开的
//p := model.Person{"lnj", 18}
// 方式一
//p := model.Person{}
//p.SetAge(18)
//fmt.Println(p)
// 方式二
//p := new(model.Person)
//p.SetAge(18)
//fmt.Println(p)
}
- 封装原则
- 将不需要对外提供的内容都隐藏起来,把属性都隐藏,提供公共的方法对其访问
-
继承性
- Go语言认为虽然继承能够提升代码的复用性, 但是会让代码腐烂, 并增加代码的复杂度.
- 所以go语言坚持了〃组合优于继承〃的原则, Go语言中所谓的继承其实是利用组合实现的(匿名结构体属性)
-
普通继承(组合)
package main
import "fmt"
type Person struct {
name string
age int
}
type Student struct {
Person // 学生继承了人的特性
score int
}
type Teacher struct {
Person // 老师继承了人的特性
Title string
}
func main() {
s := Student{Person{"lnj", 18}, 99}
//fmt.Println(s.Person.name)
fmt.Println(s.name) // 两种方式都能访问
//fmt.Println(s.Person.age)
fmt.Println(s.age) // 两种方式都能访问
fmt.Println(s.score)
}
- 继承结构中出现重名情况, 采用就近原则
package main
import "fmt"
type Person struct {
name string // 属性重名
age int
}
type Student struct {
Person
name string // 属性重名
score int
}
func main() {
s := Student{Person{"zs", 18}, "ls", 99}
fmt.Println(s.Person.name) // zs
fmt.Println(s.name) // ls
//fmt.Println(s.Person.age)
fmt.Println(s.age) // 两种方式都能访问
fmt.Println(s.score)
}
- 多重继承
package main
import "fmt"
type Object struct {
life int
}
type Person struct {
Object
name string
age int
}
type Student struct {
Person
score int
}
func main() {
s := Student{Person{Object{77}, "zs", 33}, 99}
//fmt.Println(s.Person.Object.life)
//fmt.Println(s.Person.life)
fmt.Println(s.life) // 三种方式都可以
//fmt.Println(s.Person.name)
fmt.Println(s.name) // 两种方式都能访问
//fmt.Println(s.Person.age)
fmt.Println(s.age) // 两种方式都能访问
fmt.Println(s.score)
}
package main
import "fmt"
type Object struct {
life int
}
type Person struct {
name string
age int
}
type Student struct {
Object
Person
score int
}
func main() {
s := Student{Object{77}, Person{"zs", 33}, 99}
//fmt.Println(s.Person.life)
fmt.Println(s.life) // 两种方式都可以
//fmt.Println(s.Person.name)
fmt.Println(s.name) // 两种方式都能访问
//fmt.Println(s.Person.age)
fmt.Println(s.age) // 两种方式都能访问
fmt.Println(s.score)
- 方法继承
- 在Go语言中子类不仅仅能够继承父类的属性, 还能够继承父类的方法
package main
import "fmt"
type Person struct {
name string
age int
}
// 父类方法
func (p Person)say() {
fmt.Println("name is ", p.name, "age is ", p.age)
}
type Student struct {
Person
score float32
}
func main() {
stu := Student{Person{"zs", 18}, 59.9}
stu.say()
}
- 继承中的方法重写
- 如果子类有和父类同名的方法, 那么我们称之为方法重写
package main
import "fmt"
type Person struct {
name string
age int
}
// 父类方法
func (p Person)say() {
fmt.Println("name is ", p.name, "age is ", p.age)
}
type Student struct {
Person
score float32
}
// 子类方法
func (s Student)say() {
fmt.Println("name is ", s.name, "age is ", s.age, "score is ", s.score)
}
func main() {
stu := Student{Person{"zs", 18}, 59.9}
// 和属性一样, 访问时采用就近原则
stu.say()
// 和属性一样, 方法同名时可以通过指定父类名称的方式, 访问父类方法
stu.Person.say()
}
- 注意点:
- 无论是属性继承还是方法继承, 都只能子类访问父类, 不能父类访问子类
- 多态性
- 多态就是某一类事物的多种形态
猫: 猫-->动物
狗: 狗-->动物
男人 : 男人 -->人 -->高级动物
女人 : 女人 -->人 -->高级动物
- Go语言中的多态是采用接口来实现的
package main
import "fmt"
// 1.定义接口
type Animal interface {
Eat()
}
type Dog struct {
name string
age int
}
// 2.实现接口方法
func (d Dog)Eat() {
fmt.Println(d.name, "正在吃东西")
}
type Cat struct {
name string
age int
}
// 2.实现接口方法
func (c Cat)Eat() {
fmt.Println(c.name, "正在吃东西")
}
// 3.对象特有方法
func (c Cat)Special() {
fmt.Println(c.name, "特有方法")
}
func main() {
// 1.利用接口类型保存实现了所有接口方法的对象
var a Animal
a = Dog{"旺财", 18}
// 2.利用接口类型调用对象中实现的方法
a.Eat()
a = Cat{"喵喵", 18}
a.Eat()
// 3.利用接口类型调用对象特有的方法
//a.Special() // 接口类型只能调用接口中声明的方法, 不能调用对象特有方法
if cat, ok := a.(Cat); ok{
cat.Special() // 只有对象本身才能调用对象的特有方法
}
}
- 多态优点
- 多态的主要好处就是简化了编程接口。它允许在类和类之间重用一些习惯性的命名,而不用为每一个新的方法命名一个新名字。这样,编程接口就是一些抽象的行为的集合,从而和实现接口的类的区分开来
- 多态也使得代码可以分散在不同的对象中而不用试图在一个方法中考虑到所有可能的对象。 这样使得您的代码扩展性和复用性更好一些。当一个新的情景出现时,您无须对现有的代码进行改动,而只需要增加一个新的类和新的同名方法
异常处理
- 程序运行时,发生的不被期望的事件,它阻止了程序按照程序员的预期正常执行,这就是异常
- golang中提供了两种处理异常的方式
- 一种是程序发生异常时, 将异常信息反馈给使用者
- 一种是程序发生异常时, 立刻退出终止程序继续运行
打印异常信息
- Go语言中提供了两种创建异常信息的方式
- 方式一: 通过fmt包中的Errorf函数创建错误信息, 然后打印
package main
import "fmt"
func main() {
// 1.创建错误信息
var err error = fmt.Errorf("这里是错误信息")
// 2.打印错误信息
fmt.Println(err) // 这里是错误信息
}
- 方式二: 通过errors包中的New函数创建错误信息,然后打印
package main
import "fmt"
func main() {
// 1.创建错误信息
var err error = errors.New("这里是错误信息")
// 2.打印错误信息
fmt.Println(err) // 这里是错误信息
}
- 两种创建异常信息实现原理解析
- Go语言中创建异常信息其实都是通过一个error接口实现的
- Go语言再
builtin
包中定义了一个名称叫做error的接口. 源码如下
package builtin
// 定义了一个名称叫做error的接口
// 接口中声明了一个叫做Error() 的方法
type error interface {
Error() string
}
- 在errors包中定义了一个名称叫做做errorString的结构体, 利用这个结构体实现了error接口中指定的方法
- 并且在errors 包中还提供了一个New方法, 用于创建实现了error接口的结构体对象, 并且在创建时就会把指定的字符串传递给这个结构体
// 指定包名为errors
package errors
// 定义了一个名称叫做errorString的结构体, 里面有一个字符串类型属性s
type errorString struct {
s string
}
// 实现了error接口中的Error方法
// 内部直接将结构体中保存的字符串返回
func (e \*errorString) Error() string {
return e.s
}
// 定义了一个New函数, 用于创建异常信息
// 注意: New函数的返回值是一个接口类型
func New(text string) error {
// 返回一个创建好的errorString结构体地址
return &errorString{text}
}
- fmt包中Errorf底层的实现原理其实就是在内部自动调用了errors包中的New函数
func Errorf(format string, a ...interface{}) error {
return errors.New(Sprintf(format, a...))
}
- 应用场景
package main
import "fmt"
func div(a, b int) (res int, err error) {
if(b == 0){
// 一旦传入的除数为0, 就会返回error信息
err = errors.New("除数不能为0")
}else{
res = a / b
}
return
}
func main() {
//res, err := div(10, 5)
res, err := div(10, 0)
if(err != nil){
fmt.Println(err) // 除数不能为0
}else{
fmt.Println(res) // 2
}
}
中断程序
- Go语言中提供了一个叫做panic函数, 用于发生异常时终止程序继续运行
package main
import "fmt"
func div(a, b int) (res int) {
if(b == 0){
//一旦传入的除数为0, 程序就会终止
panic("除数不能为0")
}else{
res = a / b
}
return
}
func main() {
res := div(10, 0)
fmt.Println(res)
}
- Go语言中有两种方式可以触发panic终止程序
- 我们自己手动调用panic函数
- 程序内部出现问题自动触发panic函数
package main
import "fmt"
func main() {
// 例如:数组角标越界, 就会自动触发panic
var arr = [3]int{1, 3, 5}
arr[5] = 666 // 报错
fmt.Println(arr)
// 例如:除数为0, 就会自动触发panic
var res = 10 / 0
fmt.Println(res)
}
- 除非是不可恢复性、导致系统无法正常工作的错误, 否则不建议使用panic
恢复程序
- 程序和人一样都需要具备一定的容错能力, 学会知错就改. 所以如果不是不可恢复性、导致系统无法正常工作的错误, 如果发生了panic我们需要恢复程序, 让程序继续执行,并且需要记录到底犯了什么错误
- 在Go语言中我们可以通过defer和recover来实现panic异常的捕获, 让程序继续执行
package main
import "fmt"
func div(a, b int) (res int) {
// 定义一个延迟调用的函数, 用于捕获panic异常
// 注意: 一定要在panic之前定义
defer func() {
if err := recover(); err != nil{
res = -1
fmt.Println(err) // 除数不能为0
}
}()
if(b == 0){
//err = errors.New("除数不能为0")
panic("除数不能为0")
}else{
res = a / b
}
return
}
func setValue(arr []int, index int ,value int) {
arr[index] = value
}
func main() {
res := div(10, 0)
fmt.Println(res) // -1
}
- panic注意点
- panic异常会沿着调用堆栈向外传递, 所以也可以在外层捕获
package main
import "fmt"
func div(a, b int) (res int) {
if(b == 0){
//err = errors.New("除数不能为0")
panic("除数不能为0")
}else{
res = a / b
}
return
}
func main() {
// panic异常会沿着调用堆栈向外传递, 所以也可以在外层捕获
defer func() {
if err := recover(); err != nil{
fmt.Println(err) // 除数不能为0
}
}()
div(10, 0)
}
- 多个异常,只有第一个会被捕获
package main
import "fmt"
func test1() {
// 多个异常,只有第一个会被捕获
defer func() {
if err := recover(); err != nil{
fmt.Println(err) // 异常A
}
}()
panic("异常A") // 相当于return, 后面代码不会继续执行
panic("异常B")
}
func main() {
test1(10, 0)
}
- 如果有异常写在defer中, 那么只有defer中的异常会被捕获
package main
import "fmt"
func test2() {
// 如果有异常写在defer中, 并且其它异常写在defer后面, 那么只有defer中的异常会被捕获
defer func() {
if err := recover(); err != nil{
fmt.Println(err) // 异常A
}
}()
defer func() {
panic("异常B")
}()
panic("异常A")
}
func main() {
test1(10, 0)
}
字符串相关方法
- 获取字符串长度
- 注意: Go语言编码方式是UTF-8,在UTF-8中一个汉字占3个字节
package main
import "fmt"
func main() {
str1 := "lnj"
fmt.Println(len(str1)) // 3
str2 := "公号:代码情缘"
fmt.Println(len(str2)) // 12
}
- 如果字符串中包含中文, 又想精确的计算字符串中字符的个数而不是占用的字节, 那么必须先将字符串转换为rune类型数组
- Go语言中byte用于保存字符, rune用于保存汉字
package main
import "fmt"
func main() {
str := "公号:代码情缘"
// 注意byte占1个字节, 只能保存字符不能保存汉字,因为一个汉字占用3个字节
arr1 := []byte(str) // 12
fmt.Println(len(arr1))
for \_, v := range arr1{
fmt.Printf("%c", v) // lnjæŽå—江
}
// Go语言中rune类型就是专门用于保存汉字的
arr2 := []rune(str)
fmt.Println(len(arr2)) // 6
for \_, v := range arr2{
fmt.Printf("%c", v) // lnj李南江
}
}
- 查找子串在字符串中出现的位置
- func Index(s, sep string) int
- func IndexByte(s string, c byte) int
- func IndexRune(s string, r rune) int
- func IndexAny(s, chars string) int
- func IndexFunc(s string, f func(rune) bool) int
- func LastIndex(s, sep string) int
- func LastIndexByte(s string, c byte) int
- func LastIndexAny(s, chars string) int
- func LastIndexFunc(s string, f func(rune) bool) int
package main
import (
"strings"
"fmt"
)
func main() {
// 查找`字符`在字符串中第一次出现的位置, 找不到返回-1
res := strings.IndexByte("hello 李南江", 'l')
fmt.Println(res) // 2
// 查找`汉字`OR`字符`在字符串中第一次出现的位置, 找不到返回-1
res = strings.IndexRune("hello 李南江", '李')
fmt.Println(res) // 6
res = strings.IndexRune("hello 李南江", 'l')
fmt.Println(res) // 2
// 查找`汉字`OR`字符`中任意一个在字符串中第一次出现的位置, 找不到返回-1
res = strings.IndexAny("hello 李南江", "wml")
fmt.Println(res) // 2
// 会把wmhl拆开逐个查找, w、m、h、l只要任意一个被找到, 立刻停止查找
res = strings.IndexAny("hello 李南江", "wmhl")
fmt.Println(res) // 0
// 查找`子串`在字符串第一次出现的位置, 找不到返回-1
res = strings.Index("hello 李南江", "llo")
fmt.Println(res) // 2
// 会把lle当做一个整体去查找, 而不是拆开
res = strings.Index("hello 李南江", "lle")
fmt.Println(res) // -1
// 可以查找字符也可以查找汉字
res = strings.Index("hello 李南江", "李")
fmt.Println(res) // 6
// 会将字符串先转换为[]rune, 然后遍历rune切片逐个取出传给自定义函数
// 只要函数返回true,代表符合我们的需求, 既立即停止查找
res = strings.IndexFunc("hello 李南江", custom)
fmt.Println(res) // 6
// 倒序查找`子串`在字符串第一次出现的位置, 找不到返回-1
res := strings.LastIndex("hello 李南江", "l")
fmt.Println(res) // 3
}
func custom(r rune) bool {
fmt.Printf("被调用了, 当前传入的是%c\n", r)
if r == 'o' {
return true
}
return false
}
- 判断字符串中是否包含子串
- func Contains(s, substr string) bool
- func ContainsRune(s string, r rune) bool
- func ContainsAny(s, chars string) bool
- func HasPrefix(s, prefix string) bool
- func HasSuffix(s, suffix string) bool
package main
import (
"strings"
"fmt"
)
func main() {
// 查找`子串`在字符串中是否存在, 存在返回true, 不存在返回false
// 底层实现就是调用strings.Index函数
res := strings.Contains( "hello 代码情缘", "llo")
fmt.Println(res) // true
// 查找`汉字`OR`字符`在字符串中是否存在, 存在返回true, 不存在返回false
// 底层实现就是调用strings.IndexRune函数
res = strings.ContainsRune( "hello 代码情缘", 'l')
fmt.Println(res) // true
res = strings.ContainsRune( "hello 代码情缘", '李')
fmt.Println(res) // true
// 查找`汉字`OR`字符`中任意一个在字符串中是否存在, 存在返回true, 不存在返回false
// 底层实现就是调用strings.IndexAny函数
res = strings.ContainsAny( "hello 代码情缘", "wmhl")
![img](https://img-blog.csdnimg.cn/img_convert/7aeaffc28912ebe319f0aedc18717221.png)
![img](https://img-blog.csdnimg.cn/img_convert/32f4eba18ce1d47537bc00c94140b91e.png)
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
errors.New(Sprintf(format, a...))
}
- 应用场景
package main
import "fmt"
func div(a, b int) (res int, err error) {
if(b == 0){
// 一旦传入的除数为0, 就会返回error信息
err = errors.New("除数不能为0")
}else{
res = a / b
}
return
}
func main() {
//res, err := div(10, 5)
res, err := div(10, 0)
if(err != nil){
fmt.Println(err) // 除数不能为0
}else{
fmt.Println(res) // 2
}
}
中断程序
- Go语言中提供了一个叫做panic函数, 用于发生异常时终止程序继续运行
package main
import "fmt"
func div(a, b int) (res int) {
if(b == 0){
//一旦传入的除数为0, 程序就会终止
panic("除数不能为0")
}else{
res = a / b
}
return
}
func main() {
res := div(10, 0)
fmt.Println(res)
}
- Go语言中有两种方式可以触发panic终止程序
- 我们自己手动调用panic函数
- 程序内部出现问题自动触发panic函数
package main
import "fmt"
func main() {
// 例如:数组角标越界, 就会自动触发panic
var arr = [3]int{1, 3, 5}
arr[5] = 666 // 报错
fmt.Println(arr)
// 例如:除数为0, 就会自动触发panic
var res = 10 / 0
fmt.Println(res)
}
- 除非是不可恢复性、导致系统无法正常工作的错误, 否则不建议使用panic
恢复程序
- 程序和人一样都需要具备一定的容错能力, 学会知错就改. 所以如果不是不可恢复性、导致系统无法正常工作的错误, 如果发生了panic我们需要恢复程序, 让程序继续执行,并且需要记录到底犯了什么错误
- 在Go语言中我们可以通过defer和recover来实现panic异常的捕获, 让程序继续执行
package main
import "fmt"
func div(a, b int) (res int) {
// 定义一个延迟调用的函数, 用于捕获panic异常
// 注意: 一定要在panic之前定义
defer func() {
if err := recover(); err != nil{
res = -1
fmt.Println(err) // 除数不能为0
}
}()
if(b == 0){
//err = errors.New("除数不能为0")
panic("除数不能为0")
}else{
res = a / b
}
return
}
func setValue(arr []int, index int ,value int) {
arr[index] = value
}
func main() {
res := div(10, 0)
fmt.Println(res) // -1
}
- panic注意点
- panic异常会沿着调用堆栈向外传递, 所以也可以在外层捕获
package main
import "fmt"
func div(a, b int) (res int) {
if(b == 0){
//err = errors.New("除数不能为0")
panic("除数不能为0")
}else{
res = a / b
}
return
}
func main() {
// panic异常会沿着调用堆栈向外传递, 所以也可以在外层捕获
defer func() {
if err := recover(); err != nil{
fmt.Println(err) // 除数不能为0
}
}()
div(10, 0)
}
- 多个异常,只有第一个会被捕获
package main
import "fmt"
func test1() {
// 多个异常,只有第一个会被捕获
defer func() {
if err := recover(); err != nil{
fmt.Println(err) // 异常A
}
}()
panic("异常A") // 相当于return, 后面代码不会继续执行
panic("异常B")
}
func main() {
test1(10, 0)
}
- 如果有异常写在defer中, 那么只有defer中的异常会被捕获
package main
import "fmt"
func test2() {
// 如果有异常写在defer中, 并且其它异常写在defer后面, 那么只有defer中的异常会被捕获
defer func() {
if err := recover(); err != nil{
fmt.Println(err) // 异常A
}
}()
defer func() {
panic("异常B")
}()
panic("异常A")
}
func main() {
test1(10, 0)
}
字符串相关方法
- 获取字符串长度
- 注意: Go语言编码方式是UTF-8,在UTF-8中一个汉字占3个字节
package main
import "fmt"
func main() {
str1 := "lnj"
fmt.Println(len(str1)) // 3
str2 := "公号:代码情缘"
fmt.Println(len(str2)) // 12
}
- 如果字符串中包含中文, 又想精确的计算字符串中字符的个数而不是占用的字节, 那么必须先将字符串转换为rune类型数组
- Go语言中byte用于保存字符, rune用于保存汉字
package main
import "fmt"
func main() {
str := "公号:代码情缘"
// 注意byte占1个字节, 只能保存字符不能保存汉字,因为一个汉字占用3个字节
arr1 := []byte(str) // 12
fmt.Println(len(arr1))
for \_, v := range arr1{
fmt.Printf("%c", v) // lnjæŽå—江
}
// Go语言中rune类型就是专门用于保存汉字的
arr2 := []rune(str)
fmt.Println(len(arr2)) // 6
for \_, v := range arr2{
fmt.Printf("%c", v) // lnj李南江
}
}
- 查找子串在字符串中出现的位置
- func Index(s, sep string) int
- func IndexByte(s string, c byte) int
- func IndexRune(s string, r rune) int
- func IndexAny(s, chars string) int
- func IndexFunc(s string, f func(rune) bool) int
- func LastIndex(s, sep string) int
- func LastIndexByte(s string, c byte) int
- func LastIndexAny(s, chars string) int
- func LastIndexFunc(s string, f func(rune) bool) int
package main
import (
"strings"
"fmt"
)
func main() {
// 查找`字符`在字符串中第一次出现的位置, 找不到返回-1
res := strings.IndexByte("hello 李南江", 'l')
fmt.Println(res) // 2
// 查找`汉字`OR`字符`在字符串中第一次出现的位置, 找不到返回-1
res = strings.IndexRune("hello 李南江", '李')
fmt.Println(res) // 6
res = strings.IndexRune("hello 李南江", 'l')
fmt.Println(res) // 2
// 查找`汉字`OR`字符`中任意一个在字符串中第一次出现的位置, 找不到返回-1
res = strings.IndexAny("hello 李南江", "wml")
fmt.Println(res) // 2
// 会把wmhl拆开逐个查找, w、m、h、l只要任意一个被找到, 立刻停止查找
res = strings.IndexAny("hello 李南江", "wmhl")
fmt.Println(res) // 0
// 查找`子串`在字符串第一次出现的位置, 找不到返回-1
res = strings.Index("hello 李南江", "llo")
fmt.Println(res) // 2
// 会把lle当做一个整体去查找, 而不是拆开
res = strings.Index("hello 李南江", "lle")
fmt.Println(res) // -1
// 可以查找字符也可以查找汉字
res = strings.Index("hello 李南江", "李")
fmt.Println(res) // 6
// 会将字符串先转换为[]rune, 然后遍历rune切片逐个取出传给自定义函数
// 只要函数返回true,代表符合我们的需求, 既立即停止查找
res = strings.IndexFunc("hello 李南江", custom)
fmt.Println(res) // 6
// 倒序查找`子串`在字符串第一次出现的位置, 找不到返回-1
res := strings.LastIndex("hello 李南江", "l")
fmt.Println(res) // 3
}
func custom(r rune) bool {
fmt.Printf("被调用了, 当前传入的是%c\n", r)
if r == 'o' {
return true
}
return false
}
- 判断字符串中是否包含子串
- func Contains(s, substr string) bool
- func ContainsRune(s string, r rune) bool
- func ContainsAny(s, chars string) bool
- func HasPrefix(s, prefix string) bool
- func HasSuffix(s, suffix string) bool
package main
import (
"strings"
"fmt"
)
func main() {
// 查找`子串`在字符串中是否存在, 存在返回true, 不存在返回false
// 底层实现就是调用strings.Index函数
res := strings.Contains( "hello 代码情缘", "llo")
fmt.Println(res) // true
// 查找`汉字`OR`字符`在字符串中是否存在, 存在返回true, 不存在返回false
// 底层实现就是调用strings.IndexRune函数
res = strings.ContainsRune( "hello 代码情缘", 'l')
fmt.Println(res) // true
res = strings.ContainsRune( "hello 代码情缘", '李')
fmt.Println(res) // true
// 查找`汉字`OR`字符`中任意一个在字符串中是否存在, 存在返回true, 不存在返回false
// 底层实现就是调用strings.IndexAny函数
res = strings.ContainsAny( "hello 代码情缘", "wmhl")
[外链图片转存中...(img-YhO0xLQA-1715582699689)]
[外链图片转存中...(img-jXfjWAwG-1715582699689)]
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**