变量的数据类型声明不是必须的,可以通过值的类型自动推导。
var
和:=
都可以声明变量,不能同时使用。局部变量声明后必须被使用,否则抛出异常,而全局变量没用这个限制。
常量使用
const
声明。
go// 变量声明格式
var 变量名 变量类型 = 值
// 声明多个变量
var num1, num2 int32 = 1, 2
// 批量声明,一般用于声明全局变量
var (
a int8 = 10
b string = "age"
c bool = true
)
// 简洁声明,只能用在函数内部
a, b := "age", 10
bool
:布尔类型string
:字符串int
(32或64位)、int8
、int16
、int32
、int64
:整数类型uint
(32或64位)、uint8
、uint16
、uint32
、uint64
、uintptr
:无符号整数类型float32
、float64
:浮点数complex64
、complex128
:实数和虚数byte
(uint8
的别名)、rune
(int32
的别名)complex64
/complex128
)为 0govar a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口
所有像
int
、float
、bool
、string
、array
和error
这些基本类型都属于值类型,当将一个值类型的变量赋值给另一个变量时,实际上是将值拷贝一份给新变量。而其他复杂类型,需要使用多个字节,属于引用类型,变量只会存储值的内存地址(内存地址中第一个字所在的位置),当将一个引用类型的变量赋值给另一个变量时,拷贝的只是内存地址,也就是两个变量指向同一个值。
go// 使用 &str 可获取 str 的内存地址
var str string = "string"
fmt.Println(&str)
var str2 = str
fmt.Println(&str2)
go// 常量声明使用const
const 常量名 数据类型 = 值
//多个相同类型的声明可以简写
const c_name1, c_name2 string = "value1", "value2"
//批量声明常量,可用作枚举**
const (
Unknown = 0
Female = 1
Male = 2
)
iota
特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const 关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
goconst (
a = iota // 0
b = iota // 1
c = iota // 2
)
算术运算符
+
(加)、-
(减)、*
(乘)、/
(除)、%
(取余)、++
(自增)、--
(自减)
关系运算符
==
(相等)、!=
(不相等)、>
(大于)、<
(小于)、>=
(大于或等于)、<=
(小于或等于)
逻辑运算符
&&
(与)、||
(或)、!
(非)
位运算符
&
(与)、|
(或)、^
(异或)、<<
(左移)、>>
(右移)
赋值运算符
=
、+=
、-=
、*=
、/=
、%=
、<<=
、>>=
、&=
、^=
、|=
其他运算符
&
(返回变量存储地址)、*
(声明指针变量)
可以通过使用括号来临时提升某个表达式的整体运算优先级。
不支持三元操作符(三目运算符)
可省略条件表达式括号。
持初始化语句,可定义代码块局部变量。
代码块左 括号必须在条件表达式尾部。
格式
goif 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}
if - else if - else
govar n int = 10
if n > 20 {
fmt.Println("n大于20" );
} else if n < 10 {
fmt.Println("n小于或等于20,大于10" );
} else {
fmt.Println("n小于10" );
}
可省略条件表达式括号,用于判断的变量也可以不写; case关键字后是要匹配的值或条件,可以声明多个,逗号分隔; case可省略 break,默认只会执行一个,可以使用fallthrough强制执行下一个case,不需要判断下一个case条件是否满足;
govar grade string = "B"
var marks int = 90
// switch关键字后不需要括号
switch marks {
// case关键字后是匹配的值,也可以声明多个值,逗号分隔
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C"
default: grade = "D"
}
switch {
// case后使用条件判断,也可以写多个条件,逗号分隔
case grade == "A":
fmt.Printf("优秀!\n")
case grade == "B", grade == "C":
fmt.Printf("良好\n")
case grade == "D":
fmt.Printf("及格\n")
case grade == "F":
fmt.Printf("不及格\n")
default:
fmt.Printf("差\n")
}
fmt.Printf("你的等级是 %s\n", grade)
//结果:
//优秀!
//你的等级是 A
go// fallthrough:可以强制执行下一个case,不需要判断条件
switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true")
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")
fallthrough
case true:
fmt.Println("4、case 条件语句为 true")
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}
//结果:
//2、case 条件语句为 true
//3、case 条件语句为 false
//4、case 条件语句为 true
select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。
select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。
goselect {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
go//标准的for循环,包含初始化、条件和增量
for init; condition; post { }
//只有条件,替代while
for condition { }
//死循环,替代while(true)
for { }
govar len int = 10
for i := 0; i < len; i++ {
fmt.Println(i)
}
var flag bool = true
for flag {
fmt.Printf("这是无限循环。\n")
flag = !flag
}
循环控制
类似迭代器操作,返回 (索引, 值) 或 (键, 值)。 可用于遍历string、array、slice、map等
go//字符串
var str string = "helloworld"
for _, v := range str {
fmt.Print(string(v), " ")
}
//切片
var nums []int = []int{4, 5, 6, 1, 2}
for _, v := range nums {
fmt.Print(v, " ")
}
//map
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Println(k, "->", v)
}
数组是同一种数据类型的固定长度的序列,声明是需要指定长度,定义后长度不能变。
长度是数组类型的一部分,因此,
var a[5] int
和var a[10]int
是不同的类型。数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
支持
\==
、!=
操作符,因为内存总是被初始化过的。指针数组
[n]\*T
,数组指针*[n]T
。
go// 未初始化元素值为0
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
// 通过初始化值确定数组长度
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
// 使用索引号初始化元素
var str = [5]string{3: "hello world", 4: "tom"}
//多维数组
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
数组长度
内置函数
len()
和cap()
都返回数组长度 (元素数量)
govar nums = [...]int{1, 2, 3, 4, 5}
fmt.Println(len(nums), cap(nums))
数组传参
数组传参时,是值拷贝,为会造成性能问题,通常会建议使用 slice,或数组指针
go//指针方式
func main() {
var nums = [...]int{1, 2, 3, 4, 5}
fmt.Println(max(&nums))
}
func max(nums *[5]int) int {
max := nums[0]
for i := 0; i < len(nums); i++ {
if nums[i] > max {
max = nums[i]
}
}
return max
}
go//切面方式
func main() {
var nums = [...]int{1, 2, 3, 4, 5}
fmt.Println(max(nums[:]))
}
func max(nums []int) int {
max := nums[0]
for i := 0; i < len(nums); i++ {
if nums[i] > max {
max = nums[i]
}
}
return max
}
切片并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。
切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
切片的长度可以改变,因此,切片是一个可变的数组。
切片遍历方式和数组一样,可以用
len()
求长度。表示可用元素数量,读写操作不能超过该限制。cap可以求出
slice
最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array)
,其中array
是slice
引用的数组。切片的定义:
var 变量名 []类型
,比如:var str []string var arr []int
。如果
slice == nil
,那么len()
、cap()
结果都等于 0。
定义和初始化
go//定义切片,不需要指定长度
var slice1 []int = []int{1, 2, 3, 4, 5}
//通过切割数组或切片的方式初始化
var slice2 []int = slice1[:] //全部元素
var slice3 []int = slice1[:3] //前3个元素
var slice4 []int = slice1[2:4] //从下标2到4的元素
//通过make创建,make
//make([]type, len)
//make([]type, len, cap)
var slice5 []int = make([]int, 10)
var slice6 []int = make([]int, 10, 15)
追加元素
使用内置函数append()在切片尾部追加一个或多个元素,并返回新的切片。
若追加元素后超过切片的cap,则会创建一个两倍大小的数组,并复制数据,因此创建切面时可以分配足够的空间,避免扩容。
govar slice []int = []int{1, 2, 3, 4, 5}
slice = append(slice, 6, 7, 8)
map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。
go//map的定义语法
map[KeyType]ValueType
//map类型的变量默认初始值为nil,需要使用make()函数来分配内存
make(map[KeyType]ValueType, [cap])
go//声明的时候填充元素
var m1 = map[string]string{"name": "张三", "age": "18"}
for k, v := range m1 {
fmt.Print(k, "=", v, " ")
}
//使用make分配内存
m2 := make(map[string]string, 5)
m2["name"] = "张三"
m2["age"] = "18"
for k, v := range m2 {
fmt.Print(k, "=", v, " ")
}
//判断某个键是否存在
value, ok := m1["age"]
if ok {
fmt.Println(value)
}
//删除键值对,使用内置函数delete()删除
delete(m1, "name")
之前见过的rune和byte就是类型别名。
类型别名只会在编译阶段存在,编译后将不会有这个类型。
go//自定义类型
type NewInt int
//类型别名
type MyInt = int
类似于面向对象的类,可以通过struct定义自己的类型;
go// 自定义的Person类型,可设置多个属性
type Person struct {
id int64
name string
age int8
}
func main() {
// 使用自定义的结构体,通过键值对进行初始化
var person1 Person = Person{
id: 1,
name: "张三",
}
//对属性进行修改
person1.age = 20
fmt.Println(person1)
//使用值的列表初始化,必须按照声明的属性顺序传入
var person2 Person = Person{2, "李四", 19}
fmt.Println(person2)
//通过内置函数new()初始化结构体,属性赋默认值
var person3 = new(Person)
fmt.Println(person3)
}
匿名结构体
在定义一些临时数据结构等场景下还可以使用匿名结构体。
govar person struct {id int64; name string; age int8}
Go语言的结构体没有构造函数,我们可以自己实现。
构造器的返回值是指针,避免值拷贝性能开销。
gotype Person struct {
id int64
name string
age int8
}
//定义的构造函数
func NewPerson(id int64, name string, age int8) *Person {
return &Person{id, name, age}
}
func main() {
//通过构造函数创建Person
var person *Person = NewPerson(2, "李四", 19)
fmt.Println(person)
}
Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。 接收者的概念就类似于其他语言中的
this
或者self
。
gofunc (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
gotype Person struct {
id int64
name string
age int8
}
//定义的睡觉方法
func (this Person) Sleep() {
fmt.Printf("%s正在睡觉!", this.name)
}
func main() {
var person Person = Person{2, "李四", 19}
//进行调用
person.Sleep()
}
添加方法并不限于struct,也可以给其他自定义类型添加方法。
注意事项: 非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
go//MyInt 将int定义为自定义MyInt类型
type MyInt int
//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
fmt.Println("Hello, 我是一个int。")
}
func main() {
var m1 MyInt
m1.SayHello() //Hello, 我是一个int。
m1 = 100
fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt
}
如果接收者是值传递,则方法内部的this只是副本,修改并不会影响原来的值。
如果是指针传递,则修改的是同一个。
gotype Person struct {
id int64
name string
age int8
}
// age的set方法,接收者值传递
func (this Person) SetAge1(age int8) {
this.age = age
}
// age的set方法,接收者引用传递
func (this *Person) SetAge2(age int8) {
this.age = age
}
func main() {
var person Person = Person{2, "李四", 19}
person.SetAge1(30)
fmt.Println(person) //{2 李四 19}
person.SetAge2(30)
fmt.Println(person) //{2 李四 30}
}
什么时候应该使用指针类型接收者
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
gotype Person struct {
string
int
}
func main() {
var person Person = Person{"张三", 18}
fmt.Printf("%#v", person) //main.Person{string:"张三", int:18}
}
嵌套匿名结构体
一个结构体中可以嵌套包含另一个结构体或结构体指针。
嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。
go//Address 地址结构体
type Address struct {
Province string
City string
}
//User 用户结构体
type User struct {
Name string
Gender string
Address //匿名结构体
}
func main() {
var user2 User
user2.Name = "pprof"
user2.Gender = "女"
user2.Address.Province = "黑龙江" //通过匿名结构体.字段名访问
user2.City = "哈尔滨" //直接访问匿名结构体的字段名
fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"pprof", Gender:"女", Address:main.Address{Province:"黑龙江", City:"哈尔滨"}}
}
Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。
go//Animal 动物
type Animal struct {
name string
}
func (a *Animal) move() {
fmt.Printf("%s会动!\n", a.name)
}
//Dog 狗
type Dog struct {
Feet int8
*Animal //通过嵌套匿名结构体实现继承
}
func (d *Dog) wang() {
fmt.Printf("%s会汪汪汪~\n", d.name)
}
func main() {
d1 := &Dog{
Feet: 4,
Animal: &Animal{ //注意嵌套的是结构体指针
name: "乐乐",
},
}
d1.wang() //乐乐会汪汪汪~
d1.move() //乐乐会动!
}
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
支持不定 变参。 支持多返回值。 支持命名返回参数。 支持匿名函数和闭包。 函数也是一种类型,一个函数可以赋值给变量。
不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。 不支持 重载 (overload) 不支持 默认参数 (default parameter)。
使用关键字 func 定义函数,左大括号依旧不能另起一行;
函数的返回值可以有多个,如果没有返回值,括号可以省略;
gofunc test(x, y int, s string) (int, string) {
// 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
n := x + y
return n, fmt.Sprintf(s, n)
}
函数不仅可以复制给变量,进行传参,还可以声明为类型。
go// 定义函数类型。
type FormatFunc func(s string, x, y int) string
func format(fn FormatFunc, s string, x, y int) string {
return fn(s, x, y)
}
func main() {
//匿名函数传参
s2 := format(func(s string, x, y int) string {
return fmt.Sprintf(s, x, y)
}, "%d, %d", 10, 20)
println(s2)
}
函数支持不定变参,可以传0个或多个参数。
也可以直接传递一个切片,特别注意的是在参数后加上“…”即可。
gofunc sum(nums ...int) int {
var sum int = 0
for _, v := range nums {
sum += v
}
return sum
}
func main() {
//传入多个参数
fmt.Println(sum(1, 2, 3))
var nums [3]int = [...]int{1, 2, 3}
//传入切片
fmt.Println(sum(nums[:]...))
}
返回值可以有多个,并且支持返回值命名,return时隐式返回
go//返回值可以有多个,并且支持返回值命名,return时隐式返回
func max(nums ...int) (max int, min int) {
max, min = nums[0], nums[0]
for _, v := range nums {
if v > max {
max = v
}
if v < min {
min = v
}
}
return
}
func main() {
//接收返回值,不需要的可以用 _ 忽略
ma, _ := max(1, 2, 3)
fmt.Println(ma)
//返回的多个值不能用array等容器对象接收
// marr := make([]int, 2)
// marr = max(1, 2, 3) // Error: multiple-value test() in single-value context
//返回的多个值可以作为另一个函数的参数
ma2, mi2 := max(max(1, 2, 3))
fmt.Println(ma2, mi2)
}
go//匿名函数自调用,也可以赋值给变量
func() {
println("Hello, World!")
}()
函数内嵌套函数,内部函数引用了外部函数的变量,导致被引用的变量外部函数结束后并没有被GC回收
go// 内部函数引用了外部函数的局部变量
func add(base int) func(int) int {
return func(i int) int {
base += i
return base
}
}
func main() {
//tmp1和tmp2的外部环境不同,引用的base并不是同一个,并且没有被GC回收
tmp1 := add(10)
fmt.Println(tmp1(1), tmp1(2))
tmp2 := add(100)
fmt.Println(tmp2(1), tmp2(2))
}
关键字 defer 用于注册延迟调用。
这些调用直到 return 前才被执。因此,可以用来做资源清理。
多个defer语句,按先进后出的方式执行。
defer语句中的变量,在defer声明时就决定了。
defer是先进后出,函数内最后的defer最先调用。
gofunc main() {
defer fmt.Print(1, " ")
defer fmt.Print(2, " ")
defer func() {
fmt.Print(3, " ")
}()
defer fmt.Print(4, " ")
//4 3 2 1
}
func main() {
var whatever [5]struct{}
for i := range whatever {
//defer是函数return前执行,因此deffer遇到闭包时,用到外部变量的值一定是return前的值
//而这里i变量return前的值是4,因此defer打印的全都是4,
defer func() { fmt.Print(i, " ") }()
}
//4 4 4 4 4
}
defer f.Close
gotype Test struct {
name string
}
func (t *Test) Close() {
fmt.Print(t.name, " closed ")
}
func main() {
ts := []Test{{"a"}, {"b"}, {"c"}}
for _, t := range ts {
// for循环内,每次循环t变量只是值变,而t是同一个,因此每次关闭的都是return前t的值 Test{"c"}
// defer t.Close() //c closed c closed c closed
// 将t赋值给for循环内新的变量,每次遍历都不是同一个变量,问题解决
t2 := t
defer t2.Close() //c closed b closed a closed
}
}
defer执行出现异常
若defer执行中出现异常,仍然会执行其他为执行的defer
gofunc test(x int) {
defer fmt.Println("a")
defer func() {
fmt.Println(100 / x) // div0 异常未被捕获,逐步往外传递,最终终止进程。
}()
defer fmt.Println("b")
}
func main() {
test(0)
}
//打印:
//b
//a
//panic: runtime error: integer divide by zero
函数的形参如果是值类型或者是指针类型,都只能传入对应类型的参数,而方法没有这个限制;
Golang 没有结构化异常,使用 panic() 抛出错误,recover() 捕获错误。
异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
panic(v any)
painc()用于抛出异常,异常抛出后,后面的代码将不会被执行,直到线程结束或异常被捕获;
若异常抛出前有需要执行的defer列表,则会倒序执行,可以在defer中使用recover()来捕获异常;
使用painc()抛出异常后在defer再次抛出异常时,异常会被覆盖;
recover() any
recover()用于捕获异常,必须在抛出异常前的defer中使用,否则无法捕捉异常,返回值始终是nil;
recover()处理异常后,所在函数不会被恢复,将会在上一个调用自己的函数中恢复执行;
异常抛出和捕获
gofunc test(x int) {
defer func() {
fmt.Println(recover())
}()
fmt.Println(3)
panic("错误!")
fmt.Println(4) //执行
}
func main() {
defer func() {
fmt.Println(recover())
}()
fmt.Println(1)
test(0)
fmt.Println(2)
}
//打印:
//1
//3
//错误!
//2
//<nil>
在defer中抛出异常
在defer抛出的异常可以在下一个defer中捕获;
gofunc test() {
defer func() {
fmt.Println(1, recover())
}()
defer func() {
fmt.Println(2, recover())
}()
defer func() {
panic("defer panic") //异常被覆盖
}()
panic("test panic")
}
func main() {
test()
}
//打印:
//2 defer panic
//1 <nil>
可以在可能抛出异常的位置使用匿名函数,异常捕获后保证后面的代码可以执行
异常捕获后结束的是匿名函数,当前函数仍然正常运行;
gofunc test(x, y int) {
var z int
func() {
defer func() {
if recover() != nil {
z = 0
}
}()
panic("test panic")
z = x / y
return
}()
fmt.Printf("x / y = %d\n", z)
}
func main() {
test(2, 1)
}
//打印:
//x / y = 0
除用 panic 引发中断性错误外,还可返回 error 类型错误对象来表示函数调用状态
声明异常类型
gotype error interface {
Error() string
}
errors.New 和 fmt.Errorf函数用于快速创建实现 error 接口的错误对象,通过判断错误对象实例来确定具体错误类型
goimport (
"errors"
"fmt"
)
//声明异常类型
var ErrDivByZero = errors.New("division by zero")
//第二个参数固定返回异常
func div(x, y int) (int, error) {
if y == 0 {
return 0, ErrDivByZero
}
return x / y, nil
}
func main() {
defer func() {
fmt.Println(recover())
}()
//通过switch判断异常类型进行处理
switch z, err := div(10, 0); err {
case nil:
println(z)
case ErrDivByZero:
panic(err)
}
}
//输出:
//division by zero
如何区别使用 panic 和 error 两种方式?
惯例是:导致关键流程出现不可修复性错误的使用 panic,其他使用 error。
gofunc Try(fun func(), handler func(interface{})) {
defer func() {
if err := recover(); err != nil {
handler(err)
}
}()
fun()
}
func main() {
Try(func() {
panic("test panic")
}, func(err interface{}) {
fmt.Println(err)
})
}
在Go语言中接口(interface)是一种类型,一种抽象的类型。
interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。
gotype 接口类型名 interface{
方法名1( 参数列表1 ) 返回值列表1
方法名2( 参数列表2 ) 返回值列表2
…
}
一个对象只要全部实现了接口中的方法,那么就实现了这个接口。换句话说,接口就是一个需要实现的方法列表。
go// Sayer 接口
type Sayer interface {
say()
}
type dog struct{}
// dog实现了Sayer接口
func (d dog) say() {
fmt.Println("汪汪汪")
}
type cat struct{}
// cat实现了Sayer接口
func (c cat) say() {
fmt.Println("喵喵喵")
}
func main() {
//多态
var d Sayer = dog{}
d.say() //汪汪汪
var c Sayer = cat{}
c.say() //喵喵喵
}
本文作者:牟相波
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!