基本语法——变量var
变量的使用
什么是变量
变量是为存储特定类型的值而提供给内存位置的名称。在go中声明变量有多种语法。
所以变量的本质就是一小块内存,用于存储数据,在程序运行过程中数值可以改变
声明变量
var名称类型是声明单个变量的语法。
以字母或下划线开头,由一个或多个字母、数字、下划线组成
声明一个变量
第一种,指定变量类型,声明后若不赋值,使用默认值
var name type
name = value
第二种,根据值自行判定变量类型(类型推断Type inference)
如果一个变量有一个初始值,Go将自动能够使用初始值来推断该变量的类型。因此,如果变量具有初始值,则可以省略变量声明中的类型。
var name = value
第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的(多个变量同时声明时,至少保证一个是新变量),否则会导致编译错误(简短声明)
name := value
// 例如
var a int = 10
var b = 10
c : = 10
这种方式它只能被用在函数体内,而不可以用于全局变量的声明与赋值
示例代码:
package mainvar a = "Hello"
var b string = "World"
var c boolfunc main(){println(a, b, c)
}
运行结果:
Hello World false
多变量声明
第一种,以逗号分隔,声明与赋值分开,若不赋值,存在默认值
var name1, name2, name3 type
name1, name2, name3 = v1, v2, v3
第二种,直接赋值,下面的变量类型可以是不同的类型
var name1, name2, name3 = v1, v2, v3
第三种,集合类型
var (name1 type1name2 type2
)
注意事项
如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 就是不被允许的,编译器会提示错误 no new variables on left side of :=,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。
如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a。如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 a:
func main() {var a string = "abc"fmt.Println("hello, world")
}
尝试编译这段代码将得到错误 a declared and not used
此外,单纯地给 a 赋值也是不够的,这个值必须被使用
在同一个作用域中,已存在同名的变量,则之后的声明初始化,则退化为赋值操作。但这个前提是,最少要有一个新的变量被定义,且在同一作用域,例如,下面的y就是新定义的变量
package mainimport ("fmt"
)func main() {x := 140fmt.Println(&x)x, y := 200, "abc"fmt.Println(&x, x)fmt.Print(y)
}
运行结果:
0xc04200a2b0
0xc04200a2b0 200
abc
基本语法——常量constant
常量的使用
常量声明
常量是一个简单值的标识符,在程序运行时,不会被修改的量。
const identifier [type] = value
//显式类型定义:
const b string = "abc"
//隐式类型定义:
const b = "abc"
package mainimport "fmt"func main() {const LENGTH int = 10const WIDTH int = 5 var area intconst a, b, c = 1, false, "str" //多重赋值area = LENGTH * WIDTHfmt.Printf("面积为 : %d", area)println()println(a, b, c)
}
运行结果:
面积为 : 50
1 false str
常量可以作为枚举,常量组
const (Unknown = 0Female = 1Male = 2
)
常量组中如不指定类型和初始化值,则与上一行非空常量右值相同
package mainimport ("fmt"
)func main() {const (x uint16 = 16ys = "abc"z)fmt.Printf("%T,%v\n", y, y)fmt.Printf("%T,%v\n", z, z)
}
运行结果:
uint16,16
string,abc
常量的注意事项:
-
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
-
不曾使用的常量,在编译的时候,是不会报错的
-
显示指定类型的时候,必须确保常量左右值类型一致,需要时可做显示类型转换。这与变量就不一样了,变量是可以是不同的类型值
iota关键字
iota,特殊常量,可以认为是一个可以被编译器修改的常量
iota 可以被用作枚举值:
const (a = iotab = iotac = iota
)
第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
const (a = iotabc
)
iota 用法
package mainimport "fmt"func main() {const (a = iota //0b //1c //2d = "ha" //独立值,iota += 1e //"ha" iota += 1f = 100 //iota +=1g //100 iota +=1h = iota //7,恢复计数i //8)fmt.Println(a,b,c,d,e,f,g,h,i)
}
运行结果:
0 1 2 ha ha 100 100 7 8
如果中断iota自增,则必须显式恢复。且后续自增值按行序递增
自增默认是int类型,可以自行进行显示指定类型
数字常量不会分配存储空间,无须像变量那样通过内存寻址来取值,因此无法获取地址
变量与常量的示例
package mainimport ("fmt""math"
)func main() {var a = "initial"var b, c int = 1, 2var d = truevar e float64f := float32(e)g := a + "foo"fmt.Println(a, b, c, d, e, f) // initial 1 2 true 0 0fmt.Println(g) // initialappleconst s string = "constant"const h = 500000000const i = 3e20 / hfmt.Println(s, h, i, math.Sin(h), math.Sin(i))
}
输出:
initial 1 2 true 0 0
initialfoo
constant 500000000 6e+11 -0.28470407323754404 0.7591864109375384
基本语法——数据类型
基本数据类型
以下是go中可用的基本数据类型
布尔型bool
布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true
数值型
**1、**整数型
-
int8
有符号 8 位整型 (-128 到 127)
长度:8bit -
int16
有符号 16 位整型 (-32768 到 32767) -
int32
有符号 32 位整型 (-2147483648 到 2147483647) -
int64
有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) -
uint8
无符号 8 位整型 (0 到 255)
8位都用于表示数值: -
uint16
无符号 16 位整型 (0 到 65535) -
uint32
无符号 32 位整型 (0 到 4294967295) -
uint64
无符号 64 位整型 (0 到 18446744073709551615)
int和uint:根据底层平台,表示32或64位整数。除非需要使用特定大小的整数,否则通常应该使用int来表示整数。
大小:32位系统32位,64位系统64位。
范围:-2147483648到2147483647的32位系统和-9223372036854775808到9223372036854775807的64位系统。
2、浮点型
-
float32
IEEE-754 32位浮点型数
-
float64
IEEE-754 64位浮点型数
-
complex64
32 位实数和虚数
-
complex128
64 位实数和虚数
3、其他
-
byte
类似 uint8
-
rune
类似 int32
-
uint
32 或 64 位
-
int
与 uint 一样大小
-
uintptr
无符号整型,用于存放一个指针
字符串型
字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本
var str string
str = "Hello World"
数据类型转换:Type Convert
语法格式:Type(Value)
常数:在有需要的时候,会自动转型
变量:需要手动转型 T(V)
注意点:兼容类型可以转换
复合类型(派生类型)
1、指针类型(Pointer)
2、数组类型
3、结构化类型(struct)
4、Channel 类型
5、函数类型
6、切片类型
7、接口类型(interface)
8、Map 类型
键盘输入和打印输出
打印输出
fmt包
fmt包实现了类似C语言printf和scanf的格式化I/O。格式化verb(‘verb’)源自C语言但更简单。
详见官网fmt的API:https://golang.google.cn/pkg/fmt/
导入包
import "fmt"
常用打印函数
打印:
func Print(a …interface{}) (n int, err error)
格式化打印:
func Printf(format string, a …interface{}) (n int, err error)
打印后换行
func Println(a …interface{}) (n int, err error)
格式化打印中的常用占位符:
格式化打印占位符:
%v,原样输出
%T,打印类型
%t,bool类型
%s,字符串
%f,浮点
%d,10进制的整数
%b,2进制的整数
%o,8进制
%x,%X,16进制
%x:0-9,a-f
%X:0-9,A-F
%c,打印字符
%p,打印地址
示例代码:
package mainimport ("fmt"
)func main() {a := 100 //intb := 3.14 //float64c := true // boold := "Hello World" //stringe := `Ruby` //stringf := 'A'fmt.Printf("%T,%b\n", a, a)fmt.Printf("%T,%f\n", b, b)fmt.Printf("%T,%t\n", c, c)fmt.Printf("%T,%s\n", d, d)fmt.Printf("%T,%s\n", e, e)fmt.Printf("%T,%d,%c\n", f, f, f)fmt.Println("-----------------------")fmt.Printf("%v\n", a)fmt.Printf("%v\n", b)fmt.Printf("%v\n", c)fmt.Printf("%v\n", d)fmt.Printf("%v\n", e)fmt.Printf("%v\n", f)}
运行结果:
int,1100100
float64,3.140000
bool,true
string,Hello World
string,Ruby
int32,65,A
-----------------------
100
3.14
true
Hello World
Ruby
65
键盘输入
fmt包读取键盘输入
常用方法:
- func Scan(a …interface{}) (n int, err error)
Scan 从标准输入扫描文本,读取由 空白符 分隔的值分别保存到本函数的参数中。(换行符也被视为空白符)
-
func Scanf(format string, a …interface{}) (n int, err error)
Scanf从标准输入扫描文本,按照 format 参数指定的格式去读取由空白符分隔的值保存到传递给本函数的参数中。
-
func Scanln(a …interface{}) (n int, err error)
Scanln类似Scan,它在遇到换行时停止扫描。最后一个数据后面必须有换行或者到达结束位置。
示例代码:
package mainimport ("fmt"
)func main() {var x intvar y float64fmt.Println("请输入一个整数,一个浮点类型:")fmt.Scanln(&x,&y)//读取键盘的输入,通过操作地址,赋值给x和y 阻塞式fmt.Printf("x的数值:%d,y的数值:%f\n",x,y)fmt.Scanf("%d,%f",&x,&y)fmt.Printf("x:%d,y:%f\n",x,y)
}
运行结果:
请输入一个整数,一个浮点类型:
100 3.14
x的数值:100,y的数值:3.140000
100,5.20
x:100,y:5.200000
bufio包读取
https://golang.google.cn/pkg/bufio/
bufio包中都是IO操作的方法:
先创建Reader对象,然后就可以各种读取了。
示例代码:
package mainimport ("fmt""os""bufio"
)func main() {fmt.Println("请输入一个字符串:")reader := bufio.NewReader(os.Stdin)s1, _ := reader.ReadString('\n')fmt.Println("读到的数据:", s1)}
运行效果:
请输入一个字符串:
hello wxy
读到的数据: hello wxy
解释:
reader := bufio.NewReader(os.Stdin)
:这行代码创建了一个用于读取用户输入的读取器。bufio.NewReader
函数接受一个参数os.Stdin
,它表示标准输入(通常是键盘输入)。reader
是一个读取器对象,我们可以使用它来读取用户输入的字符串。s1, _ := reader.ReadString('\n')
:这行代码使用读取器reader
从标准输入读取字符串,直到遇到换行符 ('\n'
) 为止。它将读取的字符串赋值给变量s1
。注意,这里使用了_
(下划线)来忽略函数的第二个返回值(表示可能的错误),因为我们不关心错误处理。
数组(Array)
什么是数组
Go 语言提供了数组类型的数据结构。
数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整形、字符串或者自定义类型。
数组元素可以通过索引(位置)来读取(或者修改),索引从0开始,第一个元素索引为 0,第二个索引为 1,以此类推。数组的下标取值范围是从0开始,到长度减1。
数组一旦定义后,大小不能更改。
数组的语法
声明和初始化数组
需要指明数组的大小和存储的数据类型。
var variable_name [SIZE] variable_type
示例代码:
var balance [10] float32
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小:
var balance = []float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance[4] = 50.0
数组的其他创建方式:
var a [4] float32 // 等价于:var arr2 = [4]float32{}fmt.Println(a) // [0 0 0 0]var b = [5] string{"ruby", "王二狗", "rose"}fmt.Println(b) // [ruby 王二狗 rose ]var c = [5] int{'A', 'B', 'C', 'D', 'E'} // bytefmt.Println(c) // [65 66 67 68 69]d := [...] int{1,2,3,4,5}// 根据元素的个数,设置数组的大小fmt.Println(d)//[1 2 3 4 5]e := [5] int{4: 100} // [0 0 0 0 100]fmt.Println(e)f := [...] int{0: 1, 4: 1, 9: 1} // [1 0 0 0 1 0 0 0 0 1]fmt.Println(f)
解释:
0: 1
表示将索引为 0 的位置的元素初始化为 1,4: 1
表示将索引为 4 的位置的元素初始化为 1,9: 1
表示将索引为 9 的位置的元素初始化为 1。其余位置的元素将使用默认值进行初始化,对于整数数组,默认值为 0。
访问数组元素
float32 salary = balance[9]
示例代码:
package mainimport "fmt"func main() {var n [10]int /* n 是一个长度为 10 的数组 */var i,j int/* 为数组 n 初始化元素 */ for i = 0; i < 10; i++ {n[i] = i + 100 /* 设置元素为 i + 100 */}/* 输出每个数组元素的值 */for j = 0; j < 10; j++ {fmt.Printf("Element[%d] = %d\n", j, n[j] )}
}
运行结果:
Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
Element[4] = 104
Element[5] = 105
Element[6] = 106
Element[7] = 107
Element[8] = 108
Element[9] = 109
数组的长度
通过将数组作为参数传递给len函数,可以获得数组的长度。
示例代码:
package mainimport "fmt"func main() { a := [...]float64{67.7, 89.8, 21, 78}fmt.Println("length of a is",len(a))}
运行结果:
length of a is 4
您甚至可以忽略声明中数组的长度并将其替换为…让编译器为你找到长度。这是在下面的程序中完成的。
示例代码:
package mainimport ( "fmt"
)func main() { a := [...]int{12, 78, 50} // ... makes the compiler determine the lengthfmt.Println(a)
}
运行结果:
[12 78 50]
遍历数组:
package mainimport "fmt"func main() { a := [...]float64{67.7, 89.8, 21, 78}for i := 0; i < len(a); i++ { //looping from 0 to the length of the arrayfmt.Printf("%d th element of a is %.2f\n", i, a[i])}
}
运行结果:
0 th element of a is 67.70
1 th element of a is 89.80
2 th element of a is 21.00
3 th element of a is 78.00
使用range遍历数组:
package mainimport "fmt"func main() { a := [...]float64{67.7, 89.8, 21, 78}sum := float64(0)for i, v := range a {//range returns both the index and valuefmt.Printf("%d the element of a is %.2f\n", i, v)sum += v}fmt.Println("\nsum of all elements of a",sum)
}
运行结果:
0 the element of a is 67.70
1 the element of a is 89.80
2 the element of a is 21.00
3 the element of a is 78.00sum of all elements of a 256.5
如果您只需要值并希望忽略索引,那么可以通过使用_ blank标识符替换索引来实现这一点。
for _, v := range a { //ignores index
}
多维数组
Go 语言支持多维数组,以下为常用的多维数组声明语法方式:
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
var threedim [5][10][4]int
三维数组
a = [3][4]int{ {0, 1, 2, 3} , /* 第一行索引为 0 */{4, 5, 6, 7} , /* 第二行索引为 1 */{8, 9, 10, 11} /* 第三行索引为 2 */
}
package mainimport "fmt"func main() {var twoD [2][3]intfor i := 0; i < 2; i++ {for j := 0; j < 3; j++ {twoD[i][j] = i + j}}fmt.Println("2d: ", twoD)
}
运行结果:
2d: [[0 1 2] [1 2 3]]
数组是值类型
Go中的数组是值类型,而不是引用类型。这意味着当它们被分配给一个新变量时,将把原始数组的副本分配给新变量。如果对新变量进行了更改,则不会在原始数组中反映。
package mainimport "fmt"func main() { a := [...]string{"USA", "China", "India", "Germany", "France"}b := a // a copy of a is assigned to bb[0] = "Singapore"fmt.Println("a is ", a)fmt.Println("b is ", b)
}
运行结果:
a is [USA China India Germany France]
b is [Singapore China India Germany France]
数组的大小是类型的一部分。因此[5]int和[25]int是不同的类型。因此,数组不能被调整大小。不要担心这个限制,因为切片的存在是为了解决这个问题。
package mainfunc main() { a := [3]int{5, 78, 8}var b [5]intb = a //not possible since [3]int and [5]int are distinct types
}
切片(Slice)
什么是切片
Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大
切片是一种方便、灵活且强大的包装器。切片本身没有任何数据。它们只是对现有数组的引用。
切片与数组相比,不需要设定长度,在[]中不用设定值,相对来说比较自由
从概念上面来说slice像一个结构体,这个结构体包含了三个元素:
- 指针,指向数组中slice指定的开始位置
- 长度,即slice的长度
- 最大长度,也就是slice开始位置到数组的最后位置的长度
切片的语法
定义切片
var identifier []type
切片不需要说明长度。
或使用make()函数来创建切片:
var slice1 []type = make([]type, len)
//也可以简写为
slice1 := make([]type, len)
make([]T, length, capacity)
初始化
s[0] = 1
s[1] = 2
s[2] = 3
s :=[] int {1,2,3 }
s := arr[startIndex:endIndex]
将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片(前闭后开),长度为endIndex-startIndex
s := arr[startIndex:]
缺省endIndex时将表示一直到arr的最后一个元素
s := arr[:endIndex]
缺省startIndex时将表示从arr的第一个元素开始
package mainimport ( "fmt"
)func main() { a := [5]int{76, 77, 78, 79, 80}var b []int = a[1:4] //creates a slice from a[1] to a[3]fmt.Println(b)
}
修改切片
slice没有自己的任何数据。它只是底层数组的一个表示。对slice所做的任何修改都将反映在底层数组中。
示例代码:
package mainimport ( "fmt"
)func main() { darr := [...]int{57, 89, 90, 82, 100, 78, 67, 69, 59}dslice := darr[2:5]fmt.Println("array before",darr)for i := range dslice {dslice[i]++}fmt.Println("array after",darr)
}
在这段代码中,range
遍历 dslice
时返回的是索引,而不是值。切片是对数组的引用,因此修改切片的值也会影响到原始数组。
运行结果:
array before [57 89 90 82 100 78 67 69 59]
array after [57 89 91 83 101 78 67 69 59]
当多个片共享相同的底层数组时,每个元素所做的更改将在数组中反映出来。
示例代码:
package mainimport ( "fmt"
)func main() { numa := [3]int{78, 79 ,80}nums1 := numa[:] //creates a slice which contains all elements of the arraynums2 := numa[:]fmt.Println("array before change 1",numa)nums1[0] = 100fmt.Println("array after modification to slice nums1", numa)nums2[1] = 101fmt.Println("array after modification to slice nums2", numa)
}
运行结果:
array before change 1 [78 79 80]
array after modification to slice nums1 [100 79 80]
array after modification to slice nums2 [100 101 80]
len() 和 cap()
切片的长度是切片中元素的数量。切片的容量是从创建切片的索引开始的底层数组中元素的数量。
切片是可索引的,并且可以由 len() 方法获取长度
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少
package mainimport "fmt"func main() {var numbers = make([]int,3,5)printSlice(numbers)
}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
make([]int, 3, 5)
创建了一个切片 numbers
,长度为 3,容量为 5。make
函数用于创建切片、映射和通道,并指定它们的长度和容量。
运行结果:
len=3 cap=5 slice=[0 0 0]
空切片
一个切片在未初始化之前默认为 nil,长度为 0
package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)if(numbers == nil){fmt.Printf("切片是空的")}
}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
运行结果
len=0 cap=0 slice=[]
切片是空的
package mainimport "fmt"func main() {/* 创建切片 */numbers := []int{0,1,2,3,4,5,6,7,8} printSlice(numbers)/* 打印原始切片 */fmt.Println("numbers ==", numbers)/* 打印子切片从索引1(包含) 到索引4(不包含)*/fmt.Println("numbers[1:4] ==", numbers[1:4])/* 默认下限为 0*/fmt.Println("numbers[:3] ==", numbers[:3])/* 默认上限为 len(s)*/fmt.Println("numbers[4:] ==", numbers[4:])numbers1 := make([]int,0,5)printSlice(numbers1)/* 打印子切片从索引 0(包含) 到索引 2(不包含) */number2 := numbers[:2]printSlice(number2)/* 打印子切片从索引 2(包含) 到索引 5(不包含) */number3 := numbers[2:5]printSlice(number3)}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
运行结果:
len=9 cap=9 slice=[0 1 2 3 4 5 6 7 8]
numbers == [0 1 2 3 4 5 6 7 8]
numbers[1:4] == [1 2 3]
numbers[:3] == [0 1 2]
numbers[4:] == [4 5 6 7 8]
len=0 cap=5 slice=[]
len=2 cap=9 slice=[0 1]
len=3 cap=7 slice=[2 3 4]
解释:
切片的容量(cap
)是指切片的底层数组中从切片的起始位置到数组末尾的元素数量。在这个例子中,切片 numbers
的容量与底层数组的容量相同,都是 9。这是因为我们没有指定切片的容量,Go语言会默认将切片的容量设置为底层数组中剩余的元素数量。number2
的容量是从切片的起始位置到底层数组的末尾,即剩余的元素数量。number2
的容量是 9,与底层数组的容量相同。number3
的容量是 7,因为从索引 2 开始,底层数组中还有 7 个元素。
append() 和 copy()
append 向slice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice
copy 函数copy从源slice的src中复制元素到目标dst,并且返回复制的元素的个数
append函数会改变slice所引用的数组的内容,从而影响到引用同一数组的其它slice。 但当slice中没有剩余空间(即(cap-len) == 0)时,此时将动态分配新的数组空间。返回的slice数组指针将指向这个空间,而原数组的内容将保持不变;其它引用此数组的slice则不受影响
下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法
package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)/* 允许追加空切片 */numbers = append(numbers, 0)printSlice(numbers)/* 向切片添加一个元素 */numbers = append(numbers, 1)printSlice(numbers)/* 同时添加多个元素 */numbers = append(numbers, 2,3,4)printSlice(numbers)/* 创建切片 numbers1 是之前切片的两倍容量*/numbers1 := make([]int, len(numbers), (cap(numbers))*2)/* 拷贝 numbers 的内容到 numbers1 */copy(numbers1,numbers)printSlice(numbers1)
}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
运行结果:
len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]
numbers容量为6是因为在分配底层数组时,系统会根据策略进行内存分配,通常是分配比所需容量稍微多一点的容量。所以,在这个例子中,底层数组的容量被分配为了6。
numbers1与numbers两者不存在联系,numbers发生变化时,numbers1是不会随着变化的。也就是说copy方法是不会建立两个切片的联系的
集合(Map)
什么是Map
map是Go中的内置类型,它将一个值与一个键关联起来。可以使用相应的键检索值。
Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的,也是引用类型
使用map过程中需要注意的几点:
- map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取
- map的长度是不固定的,也就是和slice一样,也是一种引用类型
- 内置的len函数同样适用于map,返回map拥有的key的数量
- map的key可以是所有可比较的类型,如布尔型、整数型、浮点型、复杂型、字符串型……也可以键。
Map的使用
使用make()创建map
可以使用内建函数 make 也可以使用 map 关键字来定义 Map:
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type/* 使用 make 函数 */
map_variable = make(map[key_data_type]value_data_type)
rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 }
如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对
package mainimport "fmt"func main() {var countryCapitalMap map[string]string/* 创建集合 */countryCapitalMap = make(map[string]string)/* map 插入 key-value 对,各个国家对应的首都 */countryCapitalMap["France"] = "Paris"countryCapitalMap["Italy"] = "Rome"countryCapitalMap["Japan"] = "Tokyo"countryCapitalMap["India"] = "New Delhi"/* 使用 key 输出 map 值 */for country := range countryCapitalMap {fmt.Println("Capital of",country,"is",countryCapitalMap[country])}/* 查看元素在集合中是否存在 */captial, ok := countryCapitalMap["United States"]/* 如果 ok 是 true, 则存在,否则不存在 */if(ok){fmt.Println("Capital of United States is", captial) }else {fmt.Println("Capital of United States is not present") }m := map[string]string{"a": "A", "b": "B"}for k, v := range m {fmt.Println(k, v) // b 8; a A}for k := range m {fmt.Println("key", k) // key a; key b
}
运行结果:
Capital of France is Paris
Capital of Italy is Rome
Capital of Japan is Tokyo
Capital of India is New Delhi
Capital of United States is not present
a A
b B
key a
key b
delete() 函数
delete(map, key) 函数用于删除集合的元素, 参数为 map 和其对应的 key。删除函数不返回任何值。
package mainimport "fmt"func main() { /* 创建 map */countryCapitalMap := map[string] string {"France":"Paris","Italy":"Rome","Japan":"Tokyo","India":"New Delhi"}fmt.Println("原始 map") /* 打印 map */for country := range countryCapitalMap {fmt.Println("Capital of",country,"is",countryCapitalMap[country])}/* 删除元素 */delete(countryCapitalMap,"France");fmt.Println("Entry for France is deleted") fmt.Println("删除元素后 map") /* 打印 map */for country := range countryCapitalMap {fmt.Println("Capital of",country,"is",countryCapitalMap[country])}
}
运行结果:
原始 map
Capital of France is Paris
Capital of Italy is Rome
Capital of Japan is Tokyo
Capital of India is New Delhi
Entry for France is deleted
删除元素后 map
Capital of Italy is Rome
Capital of Japan is Tokyo
Capital of India is New Delhi
ok-idiom
我们可以通过key获取map中对应的value值。语法为:
map[key]
但是当key如果不存在的时候,我们会得到该value值类型的默认值,比如string类型得到空字符串,int类型得到0。但是程序不会报错。
所以我们可以使用ok-idiom获取值,可知道key/value是否存在
value, ok := map[key]
示例代码:
package mainimport ("fmt"
)func main() {m := make(map[string]int)m["a"] = 1x, ok := m["b"]fmt.Println(x, ok)x, ok = m["a"]fmt.Println(x, ok)
}
运行结果:
0 false
1 true
map的长度
使用len函数可以确定map的长度。
len(map) // 可以得到map的长度
map是引用类型
与切片相似,映射是引用类型。当将映射分配给一个新变量时,它们都指向相同的内部数据结构。因此,一个的变化会反映另一个。
示例代码:
package mainimport ( "fmt"
)func main() { personSalary := map[string]int{"steve": 12000,"jamie": 15000,}personSalary["mike"] = 9000fmt.Println("Original person salary", personSalary)newPersonSalary := personSalarynewPersonSalary["mike"] = 18000fmt.Println("Person salary changed", personSalary)}
运行结果:
Original person salary map[steve:12000 jamie:15000 mike:9000]
Person salary changed map[steve:12000 jamie:15000 mike:18000]
map不能使用==操作符进行比较。==只能用来检查map是否为空。否则会报错:invalid operation: map1 == map2 (map can only be comparedto nil)
字符串(string)
什么是string
Go中的字符串是一个字节的切片。可以通过将其内容封装在“”中来创建字符串。Go中的字符串是Unicode兼容的,并且是UTF-8编码的。
示例代码:
package mainimport ( "fmt"
)func main() { name := "Hello World"fmt.Println(name)
}
string的使用
访问字符串中的单个字节
package mainimport ("fmt"
)func main() {s := "Hello World"for i := 0; i < len(s); i++ {fmt.Printf("%d ", s[i])}fmt.Printf("\n")for i := 0; i < len(s); i++ {fmt.Printf("%c ", s[i])}
}
%c:相应Unicode码点所表示的字符
运行结果:
72 101 108 108 111 32 87 111 114 108 100
H e l l o W o r l d
函数
什么是函数
函数是执行特定任务的代码块。
函数的声明
go 语言至少有一个 main 函数
语法格式:
func funcName(parametername type1, parametername type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
//返回多个值
return value1, value2
}
- func:函数由 func 开始声明
- funcName:函数名称,函数名和参数列表一起构成了函数签名。
- parametername type:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
- output1 type1, output2 type2:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
- 上面返回值声明了两个变量 output1 和 output2,如果你不想声明也可以,直接就两个类型。
- 如果只有一个返回值且不声明返回值变量,那么你可以省略包括返回值的括号(即一个返回值可以不声明返回类型)
- 函数体:函数定义的代码集合。
函数的使用
示例代码:
package mainimport "fmt"func add(a int, b int) int {return a + b
}func add2(a, b int) int {return a + b
}func exists(m map[string]string, k string) (v string, ok bool) {v, ok = m[k]return v, ok
}func main() {res := add(1, 2)fmt.Println(res) // 3v, ok := exists(map[string]string{"a": "A"}, "a")fmt.Println(v, ok) // A True}
运行结果:
3
A true
函数的参数
参数的使用
形式参数:定义函数时,用于接收外部传入的数据,叫做形式参数,简称形参。
实际参数:调用函数时,传给形参的实际的数据,叫做实际参数,简称实参。
函数调用:
A:函数名称必须匹配
B:实参与形参必须一一对应:顺序,个数,类型
可变参
Go 函数支持变参。接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:
func myfunc(arg ...int) {}
arg ...int
告诉 Go 这个函数接受不定数量的参数。注意,这些参数的类型全部是 int。在函数体中,变量 arg 是一个 int 的 slice:
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}
参数传递
go 语言函数的参数也是存在值传递和引用传递
函数运用场景
值传递
package mainimport ("fmt""math"
)func main(){/* 声明函数变量 */getSquareRoot := func(x float64) float64 {return math.Sqrt(x)}/* 使用函数 */fmt.Println(getSquareRoot(9))}
引用传递
这就牵扯到了所谓的指针。我们知道,变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。
只有 add1 函数知道 x 变量所在的地址,才能修改 x 变量的值。所以我们需要将 x 所在地址 & x 传入函数,并将函数的参数的类型由 int 改为 * int,即改为指针类型,才能在函数中修改 x 变量的值。此时参数仍然是按 copy 传递的,只是 copy 的是一个指针。
package mainimport "fmt"// 简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,*a = *a + 1 // 修改了a的值return *a // 返回新值
}
func main() {x := 3fmt.Println("x = ", x) // 应该输出 "x = 3"x1 := add1(&x) // 调用 add1(&x) 传x的地址fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"fmt.Println("x = ", x) // 应该输出 "x = 4"
}
输出:
x = 3
x+1 = 4
x = 4
- 传指针使得多个函数能操作同一个对象。
- 传指针比较轻量级 (8bytes), 只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话,在每次 copy 上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
- Go 语言中 slice,map 这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变 slice 的长度,则仍需要取地址传递指针)
函数的返回值
什么是函数的返回值
一个函数被调用后,返回给调用处的执行结果,叫做函数的返回值。
调用处需要使用变量接收该结果
一个函数可以返回多个值
一个函数可以没有返回值,也可以有一个返回值,也可以有返回多个值。
package mainimport "fmt"func swap(x, y string) (string, string) {return y, x
}func main() {a, b := swap("Mahesh", "Kumar")fmt.Println(a, b)
}
func SumAndProduct(A, B int) (add int, Multiplied int) {
add = A+B
Multiplied = A*B
return
}
空白标识符
_是 Go 中的空白标识符。它可以代替任何类型的任何值。让我们看看这个空白标识符的用法。
比如 rectProps 函数返回的结果是面积和周长,如果我们只要面积,不要周长,就可以使用空白标识符。
示例代码:
package mainimport ( "fmt"
)func rectProps(length, width float64) (float64, float64) { var area = length * widthvar perimeter = (length + width) * 2return area, perimeter
}
func main() { area, _ := rectProps(10.8, 5.6) // perimeter is discardedfmt.Printf("Area %f ", area)
}
变量的作用域
作用域:变量可以使用的范围。
局部变量
一个函数内部定义的变量,就叫做局部变量
变量在哪里定义,就只能在哪个范围使用,超出这个范围,我们认为变量就被销毁了。
全局变量
一个函数外部定义的变量,就叫做全局变量
所有的函数都可以使用,而且共享这一份数据
递归函数
一个函数自己调用自己,就叫做递归调用,一个递归函数一定要有出口,否则会陷入死循环
defer
延迟是什么?
即延迟(defer)语句,延迟语句被用于执行一个函数调用,在这个函数之前,延迟语句返回。
延迟函数
你可以在函数中添加多个 defer 语句。当函数执行到最后时,这些 defer 语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。
- 如果有很多调用 defer,那么 defer 是采用
后进先出
模式 - 在离开所在的方法时,执行(报错的时候也会执行)
func ReadWrite() bool {file.Open("file")defer file.Close()if failureX {return false} if failureY {return false} return true
}
最后才执行 file.Close()
示例代码:
package mainimport "fmt"func main() {a := 1b := 2defer fmt.Println(b)fmt.Println(a)
}
运行结果:
1
2
示例代码:
package mainimport ( "fmt"
)func finished() { fmt.Println("Finished finding largest")
}func largest(nums []int) { defer finished() fmt.Println("Started finding largest")max := nums[0]for _, v := range nums {if v > max {max = v}}fmt.Println("Largest number in", nums, "is", max)
}func main() { nums := []int{78, 109, 2, 563, 300}largest(nums)
}
运行结果:
Started finding largest
Largest number in [78 109 2 563 300] is 563
Finished finding largest
延迟方法
延迟并不仅仅局限于函数。延迟一个方法调用也是完全合法的。让我们编写一个小程序来测试这个。
示例代码:
package mainimport ("fmt"
)type person struct {firstName stringlastName string
}func (p person) fullName() {fmt.Printf("%s %s", p.firstName, p.lastName)
}func main() {p := person{firstName: "John",lastName: "Smith",}defer p.fullName()fmt.Printf("Welcome ")
}
运行结果:
Welcome John Smith
延迟参数
延迟函数的参数在执行延迟语句时被执行,而不是在执行实际的函数调用时执行。
让我们通过一个例子来理解这个问题。
示例代码:
package mainimport ( "fmt"
)func printA(a int) { fmt.Println("value of a in deferred function", a)
}
func main() { a := 5defer printA(a)a = 10fmt.Println("value of a before deferred function call", a)}
运行结果:
value of a before deferred function call 10
value of a in deferred function 5
堆栈的推迟
当一个函数有多个延迟调用时,它们被添加到一个堆栈中,并在 Last In First Out(LIFO)后进先出的顺序中执行。
编写一个小程序,它使用一堆 defers 打印一个字符串。示例代码:
package mainimport ( "fmt"
)func main() { name := "Naveen"fmt.Printf("Orignal String: %s\n", string(name))fmt.Printf("Reversed String: ")for _, v := range []rune(name) {defer fmt.Printf("%c", v)}
}
解释:
在 Go 语言中,rune
是一个整数类型别名,用于表示 Unicode 字符。它实际上是一个 32 位的整数,可以存储任何 Unicode 码点的值。在 Go 中,字符串是由一系列的 Unicode 字符组成的。每个 Unicode 字符可以由一个或多个字节表示。使用 rune
类型,我们可以按字符而不是字节来处理字符串。rune
类型的主要作用是允许我们在字符串中按字符进行迭代、索引和操作。当我们需要遍历字符串中的每个字符时,特别是在涉及多字节字符(如中文、日文或特殊符号)的情况下,使用rune
类型可以确保正确处理每个字符。在上面的代码示例中,我们使用 []rune(name)
将字符串 name
转换为一个 rune
切片,以便我们可以按字符迭代处理它。这样可以确保逆序打印字符串中的每个字符,而不仅仅是按字节逆序打印。
运行结果:
Orignal String: Naveen
Reversed String: neevaN
defer 注意点
defer 函数:
当外围函数中的语句正常执行完毕时,只有其中所有的延迟函数都执行完毕,外围函数才会真正的结束执行。
当执行外围函数中的 return 语句时,只有其中所有的延迟函数都执行完毕后,外围函数才会真正返回。
当外围函数中的代码引发运行恐慌时,只有其中所有的延迟函数都执行完毕后,该运行时恐慌才会真正被扩展至调用函数。
程序的流程结构
程序的流程控制结构一共有三种:顺序结构,选择结构,循环结构。
顺序结构:从上向下,逐行执行。
选择结构:条件满足,某些代码才会执行。0-1次
分支语句:if,switch,select
循环结构:条件满足,某些代码会被反复的执行多次。0-N次
循环语句:for
条件语句
If语句
语法格式:
if 布尔表达式 {/* 在布尔表达式为 true 时执行 */
}
if 布尔表达式 {/* 在布尔表达式为 true 时执行 */
} else {/* 在布尔表达式为 false 时执行 */
}
if 布尔表达式1 {/* 在布尔表达式1为 true 时执行 */
} else if 布尔表达式2{/* 在布尔表达式1为 false ,布尔表达式2为true时执行 */
} else{/* 在上面两个布尔表达式都为false时,执行*/
}
示例代码:
package mainimport "fmt"func main() {/* 定义局部变量 */var a int = 10/* 使用 if 语句判断布尔表达式 */if a < 20 {/* 如果条件为 true 则执行以下语句 */fmt.Printf("a 小于 20\n" )}fmt.Printf("a 的值为 : %d\n", a)
}
如果其中包含一个可选的语句组件(在评估条件之前执行),则还有一个变体。它的语法是
if statement; condition {
}if condition{
}
示例代码:
package mainimport "fmt"func main() {if 7%2 == 0 {fmt.Println("7 is even")} else {fmt.Println("7 is odd")}if 8%4 == 0 {fmt.Println("8 is divisible by 4")}if num := 9; num < 0 {fmt.Println(num, "is negative")} else if num < 10 {fmt.Println(num, "has 1 digit")} else {fmt.Println(num, "has multiple digits")}
}
需要注意的是,num的定义在if里,那么只能够在该if…else语句块中使用,否则编译器会报错的。
switch语句:“开关”
switch是一个条件语句,它计算表达式并将其与可能匹配的列表进行比较,并根据匹配执行代码块。它可以被认为是一种惯用的方式来写多个if else子句。
switch 语句用于基于不同条件执行不同动作,每一个 case 分支都是唯一的,从上直下逐一测试,直到匹配为止。
switch 语句执行的过程从上至下,直到找到匹配项,匹配项后面也不需要再加break。
而如果switch没有表达式,它会匹配true
Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。
变量 var1 可以是任何类型,而 val1 和 val2 则可以是同类型的任意值。类型不被局限于常量或整数,但必须是相同的类型;或者最终结果为相同类型的表达式。
可以同时测试多个可能符合条件的值,使用逗号分割它们,例如:case val1, val2, val3。
switch var1 {case val1:...case val2:...default:...
}
示例代码:
package mainimport ("fmt""time"
)func main() {/* 定义局部变量 */var grade = "B"var marks = 90switch marks {case 90:grade = "A"case 80:grade = "B"case 50, 60, 70:grade = "C" //case 后可以由多个数值default:grade = "D"}switch {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)t := time.Now()switch {case t.Hour() < 12:fmt.Println("It's before noon")default:fmt.Println("It's after noon")}
}
输出:
优秀!
你的等级是 A
It's before noon
fallthrough
如需贯通后续的case,就添加fallthrough
package mainimport ("fmt"
)func main() {switch x := 5; x {default:fmt.Println(x)case 5:x += 10fmt.Println(x)fallthroughcase 6:x += 20fmt.Println(x)}}
运行结果:
15
35
解释:
根据switch语句的逻辑,首先会执行default分支,但是由于我们没有在default分支中写入任何代码,所以会直接跳过。在case 5分支的最后,我们使用了fallthrough关键字。fallthrough关键字的作用是强制执行下一个case分支的代码,而不进行条件判断。
在switch
语句中,default
用于处理没有匹配到任何case
的情况。当switch
表达式的值与所有的case
都不匹配时,程序会执行default
分支中的代码。
default
分支是可选的,也就是说可以选择是否在switch
语句中包含它。如果没有default
分支,而且没有任何一个case
匹配到switch
表达式的值,那么switch
语句将不会执行任何代码。
default
分支通常被用作最后一个分支,用于处理不常见或者意外的情况。它可以用来提供一个默认的处理逻辑,或者给用户一个错误提示。
以下是一个示例,展示了default
的使用:
package mainimport "fmt"func main() {num := 10switch num {case 1, 2, 3:fmt.Println("小数")case 4, 5, 6:fmt.Println("中数")case 7, 8, 9:fmt.Println("大数")default:fmt.Println("未知数")}
}
在上面的示例中,如果num
的值不属于1到9之间的任何一个数,那么就会执行default
分支,并输出"未知数"。default
分支可以用来处理一些边缘情况或者未预料到的情况,确保程序的健壮性。
case中的表达式是可选的,可以省略。如果该表达式被省略,则被认为是switch true,并且每个case表达式都被计算为true,并执行相应的代码块。
示例代码:
package mainimport ( "fmt"
)func main() { num := 75switch { // expression is omittedcase num >= 0 && num <= 50:fmt.Println("num is greater than 0 and less than 50")case num >= 51 && num <= 100:fmt.Println("num is greater than 51 and less than 100")case num >= 101:fmt.Println("num is greater than 100")}
}
输出:
num is greater than 51 and less than 100
switch的注意事项
- case后的常量值不能重复
- case后可以有多个常量值
- fallthrough应该是某个case的最后一行。如果它出现在中间的某个地方,编译器就会抛出错误。
Type Switch
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
switch x.(type){case type:statement(s); case type:statement(s); /* 你可以定义任意个数的case */default: /* 可选 */statement(s);
}
示例代码:
package mainimport "fmt"func main() {var x interface{}switch i := x.(type) {case nil: fmt.Printf(" x 的类型 :%T",i) case int: fmt.Printf("x 是 int 型") case float64:fmt.Printf("x 是 float64 型") case func(int) float64:fmt.Printf("x 是 func(int) 型") case bool, string:fmt.Printf("x 是 bool 或 string 型" ) default:fmt.Printf("未知型") }
}
运行结果:
x 的类型 :<nil>
解释:
在这段代码中,变量x
的类型被声明为interface{}
,它是一个空接口。空接口可以表示任意类型的值,包括nil
。当我们使用类型断言x.(type)
来获取x
的具体类型时,如果x
的值是nil
,那么case nil
分支会匹配成功。这里需要注意的是,nil
是一个特殊的值,表示一个指针类型或接口类型的零值,表示该指针或接口不指向任何具体的对象。在这种情况下,我们可以将其视为一种特殊的类型。因此,case nil
分支被用来处理x
为nil
的情况。
循环语句
循环语句表示条件满足,可以反复的执行某段代码。
for是唯一的循环语句。(Go没有while循环)
for语句
语法结构:
for init; condition; post { }
初始化语句只执行一次。在初始化循环之后,将检查该条件。如果条件计算为true,那么{}中的循环体将被执行,然后是post语句。post语句将在循环的每次成功迭代之后执行。在执行post语句之后,该条件将被重新检查。如果它是正确的,循环将继续执行,否则循环终止。
示例代码:
package mainimport ( "fmt"
)func main() { for i := 1; i <= 10; i++ {fmt.Printf(" %d",i)}
}
在for循环中声明的变量仅在循环范围内可用。因此,i不能在外部访问循环。
所有的三个组成部分,即初始化、条件和post都是可选的。
for condition { }
效果与while相似
for { }
效果与for(;😉 一样
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环
for key, value := range oldMap {newMap[key] = value
}
package mainimport "fmt"func main() {var b int = 9var a intnumbers := [6]int{1, 2, 3, 5} /* for 循环 */for a := 0; a < 5; a++ {fmt.Printf("a 的值为: %d\n", a)}for a < b {a++fmt.Printf("a 的值为: %d\n", a)}for i,x:= range numbers {fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)}
}
运行结果:
a 的值为: 0
a 的值为: 1
a 的值为: 2
a 的值为: 3
a 的值为: 4
a 的值为: 1
a 的值为: 2
a 的值为: 3
a 的值为: 4
a 的值为: 5
a 的值为: 6
a 的值为: 7
a 的值为: 8
a 的值为: 9
第 0 位 x 的值 = 1
第 1 位 x 的值 = 2
第 2 位 x 的值 = 3
第 3 位 x 的值 = 5
第 4 位 x 的值 = 0
第 5 位 x 的值 = 0
多层for循环
for循环中又有循环嵌套,就表示多层循环了。
跳出循环的语句
break语句
break:跳出循环体。break语句用于在结束其正常执行之前突然终止for循环
示例代码:
package mainimport ( "fmt"
)func main() { for i := 1; i <= 10; i++ {if i > 5 {break //loop is terminated if i > 5}fmt.Printf("%d ", i)}fmt.Printf("\nline after for loop")
}
输出:
1 2 3 4 5
line after for loop
continue语句
continue:跳出一次循环。continue语句用于跳过for循环的当前迭代。在continue语句后面的for循环中的所有代码将不会在当前迭代中执行。循环将继续到下一个迭代。
示例代码:
package mainimport ( "fmt"
)func main() { for i := 1; i <= 10; i++ {if i%2 == 0 {continue}fmt.Printf("%d ", i)}
}
输出:
1 3 5 7 9
goto语句
goto:可以无条件地转移到过程中指定的行。
语法结构:
goto label;
..
..
label: statement;
package mainimport "fmt"func main() {/* 定义局部变量 */var a = 10/* 循环 */
LOOP:for a < 20 {if a == 15 {/* 跳过迭代 */a = a + 1goto LOOP}fmt.Printf("a的值为 : %d\n", a)a++}
}
输出:
a的值为 : 10
a的值为 : 11
a的值为 : 12
a的值为 : 13
a的值为 : 14
a的值为 : 16
a的值为 : 17
a的值为 : 18
a的值为 : 19
统一错误处理
多处错误处理存在代码重复时是非常棘手的,例如:
err := firstCheckError()if err != nil {goto onExit}err = secondCheckError()if err != nil {goto onExit}fmt.Println("done")return
onExit:fmt.Println(err)exitProcess()
生成随机数
示例代码:
package mainimport ("fmt""math/rand""time"
)func main() {num1 := rand.Int()fmt.Println(num1)for i := 0; i < 10; i++ {num := rand.Intn(10)fmt.Println(num)}rand.Seed(1000)num2 := rand.Intn(10)fmt.Println("--->", num2)t1 := time.Now()fmt.Println(t1)fmt.Printf("%T\n", t1)timeStamp1 := t1.Unix()fmt.Println(timeStamp1)timestamp2 := t1.UnixNano()fmt.Println(timestamp2)rand.Seed(time.Now().UnixNano())for i := 0; i < 10; i++ {fmt.Println("--->", rand.Intn(100))}num3 := rand.Intn(46) + 3fmt.Println(num3)num4 := rand.Intn(62) + 15fmt.Println(num4)
}
输出:
2359980755445512729
6
0
9
3
2
---> 5
2023-06-02 12:00:50.6796224 +0800 CST m=+0.001542201
time.Time
1685678450
1685678450679622400
---> 74
---> 79
---> 18
---> 21
---> 68
---> 41
---> 33
---> 31
---> 26
---> 37
15
解释:
在随机数生成中,种子(seed)是用于初始化随机数生成器的值。种子决定了随机数序列的起始点。在某种程度上,相同种子会生成相同的随机数序列。
在上述代码中,rand.Seed()
方法用于设置随机数生成器的种子。如果没有显式地设置种子,Go语言的math/rand
包默认使用一个固定的种子,这意味着每次程序运行时都会生成相同的随机数序列。这在某些情况下可能不是我们所期望的。
通过调用rand.Seed()
方法并传入一个不同的种子值,我们可以改变随机数生成器的起始点,从而产生不同的随机数序列。通常情况下,我们会使用当前时间的纳秒级时间戳作为种子,以确保每次运行程序时都能生成不同的随机数序列。
在代码中,rand.Seed(1000)
将种子设置为固定值1000,因此后续生成的随机数序列将始终相同。而rand.Seed(time.Now().UnixNano())
使用当前时间的纳秒级时间戳作为种子,可以产生不同的随机数序列。
但是自从Go 1.2 0以来,rand.Seed就已经被弃用了!
指针
指针的概念
指针是存储另一个变量的内存地址的变量。
我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。
一个指针变量可以指向任何一个值的内存地址。
获取变量的地址
Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
package mainimport "fmt"func main() {var a int = 10 fmt.Printf("变量的地址: %x\n", &a )
}
运行结果:
变量的地址: 20818a220
声明指针
声明指针,*T是指针变量的类型,它指向T类型的值。
var var_name *var-type
var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
示例代码:
package mainimport "fmt"func main() {var a int= 20 /* 声明实际变量 */var ip *int /* 声明指针变量 */ip = &a /* 指针变量的存储地址 */fmt.Printf("a 变量的地址是: %x\n", &a )/* 指针变量的存储地址 */fmt.Printf("ip 变量的存储地址: %x\n", ip )/* 使用指针访问值 */fmt.Printf("*ip 变量的值: %d\n", *ip )
}
运行结果:
a 变量的地址是: 20818a220
ip 变量的存储地址: 20818a220
*ip 变量的值: 20
示例代码:
package mainimport "fmt"type name int8
type first struct {a intb boolname
}func main() {var a = first{1, false, 2}var b *first = &afmt.Println(a.b, a.a, a.name, &a, b.a, &b, (*b).a)
}
运行结果:
false 1 2 &{1 false 2} 1 0xc042068018 1
获取指针地址在指针变量前加&的方式
空指针
Go 空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
nil 指针也称为空指针。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
一个指针变量通常缩写为 ptr。
空指针判断:
if(ptr != nil) /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */
获取指针的值
获取一个指针意味着访问指针指向的变量的值。语法是:*a
示例代码:
package main
import ( "fmt"
)func main() { b := 255a := &bfmt.Println("address of b is", a)fmt.Println("value of b is", *a)
}
操作指针改变变量的数值
示例代码:
package mainimport ( "fmt"
)func main() { b := 255a := &bfmt.Println("address of b is", a)fmt.Println("value of b is", *a)*a++fmt.Println("new value of b is", b)
}
运行结果
address of b is 0x1040a124
value of b is 255
new value of b is 256
使用指针传递函数的参数
示例代码
package mainimport ( "fmt"
)func change(val *int) { *val = 55
}
func main() { a := 58fmt.Println("value of a before function call is",a)b := &achange(b)fmt.Println("value of a after function call is", a)
}
运行结果
value of a before function call is 58
value of a after function call is 55
不要将一个指向数组的指针传递给函数。使用切片。
假设我们想对函数内的数组进行一些修改,并且对调用者可以看到函数内的数组所做的更改。一种方法是将一个指向数组的指针传递给函数。
package mainimport ( "fmt"
)func modify(arr *[3]int) { (*arr)[0] = 90
}func main() { a := [3]int{89, 90, 91}modify(&a)fmt.Println(a)
}
运行结果
[90 90 91]
示例代码:
package mainimport ( "fmt"
)func modify(arr *[3]int) { arr[0] = 90
}func main() { a := [3]int{89, 90, 91}modify(&a)fmt.Println(a)
}
运行结果
[90 90 91]
虽然将指针传递给一个数组作为函数的参数并对其进行修改,但这并不是实现这一目标的惯用方法。我们有切片。
示例代码:
package mainimport ( "fmt"
)func modify(sls []int) { sls[0] = 90
}func main() { a := [3]int{89, 90, 91}modify(a[:])fmt.Println(a)
}
运行结果:
[90 90 91]
Go不支持指针算法。
package mainfunc main() {
b := [...]int{109, 110, 111}
p := &b
p++
}
nvalid operation: p++ (non-numeric type *[3]int)
指针数组
package mainimport "fmt"const MAX int = 3func main() {a := []int{10,100,200}var i intfor i = 0; i < MAX; i++ {fmt.Printf("a[%d] = %d\n", i, a[i] )}
}
输出结果
a[0] = 10
a[1] = 100
a[2] = 200
有一种情况,我们可能需要保存数组,这样我们就需要使用到指针。
package mainimport "fmt"const MAX int = 3func main() {a := []int{10, 100, 200}var i intvar ptr [MAX]*intfor i = 0; i < MAX; i++ {ptr[i] = &a[i] /* 整数地址赋值给指针数组 */}for i = 0; i < MAX; i++ {fmt.Printf("a[%d] = %d\n", i, *ptr[i])}
}
输出结果
a[0] = 10
a[1] = 100
a[2] = 200
指针的指针
指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
var ptr **int;
package mainimport "fmt"func main() {var a intvar ptr *intvar pptr **inta = 3000/* 指针 ptr 地址 */ptr = &a/* 指向指针 ptr 地址 */pptr = &ptr/* 获取 pptr 的值 */fmt.Printf("变量 a = %d\n", a )fmt.Printf("指针变量 *ptr = %d\n", *ptr )fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
输出结果
变量 a = 3000
指针变量 *ptr = 3000
指向指针的指针变量 **pptr = 3000
指针作为函数参数
package mainimport "fmt"func main() {/* 定义局部变量 */var a int = 100var b int= 200fmt.Printf("交换前 a 的值 : %d\n", a )fmt.Printf("交换前 b 的值 : %d\n", b )/* 调用函数用于交换值* &a 指向 a 变量的地址* &b 指向 b 变量的地址*/swap(&a, &b);fmt.Printf("交换后 a 的值 : %d\n", a )fmt.Printf("交换后 b 的值 : %d\n", b )
}func swap(x *int, y *int) {var temp inttemp = *x /* 保存 x 地址的值 */*x = *y /* 将 y 赋值给 x */*y = temp /* 将 temp 赋值给 y */
}
输出结果
交换前 a 的值 : 100
交换前 b 的值 : 200
交换后 a 的值 : 200
交换后 b 的值 : 100
结构体
什么是结构体
Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。
结构体的定义和初始化
type struct_variable_type struct {member definition;member definition;...member definition;
}
一旦定义了结构体类型,它就能用于变量的声明
variable_name := structure_variable_type {value1, value2...valuen}
初始化结构体
// 1.按照顺序提供初始化值
P := person{"Tom", 25}
// 2.通过field:value的方式初始化,这样可以任意顺序
P := person{age:24, name:"Tom"}
// 3.new方式,未设置初始值的,会赋予类型的默认初始值
p := new(person)
p.age=24
结构体的访问
访问结构体成员(访问结构的各个字段)
通过点.操作符用于访问结构的各个字段。
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int
}func main() {var Book1 Books /* 声明 Book1 为 Books 类型 */var Book2 Books /* 声明 Book2 为 Books 类型 *//* book 1 描述 */Book1.title = "Go 语言"Book1.author = "www.runoob.com"Book1.subject = "Go 语言教程"Book1.book_id = 6495407/* book 2 描述 */Book2.title = "Python 教程"Book2.author = "www.runoob.com"Book2.subject = "Python 语言教程"Book2.book_id = 6495700/* 打印 Book1 信息 */fmt.Printf( "Book 1 title : %s\n", Book1.title)fmt.Printf( "Book 1 author : %s\n", Book1.author)fmt.Printf( "Book 1 subject : %s\n", Book1.subject)fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id)/* 打印 Book2 信息 */fmt.Printf( "Book 2 title : %s\n", Book2.title)fmt.Printf( "Book 2 author : %s\n", Book2.author)fmt.Printf( "Book 2 subject : %s\n", Book2.subject)fmt.Printf( "Book 2 book_id : %d\n", Book2.book_id)
}
运行结果:
Book 1 title : Go 语言
Book 1 author : www.runoob.com
Book 1 subject : Go 语言教程
Book 1 book_id : 6495407
Book 2 title : Python 教程
Book 2 author : www.runoob.com
Book 2 subject : Python 语言教程
Book 2 book_id : 6495700
结构体指针
指针指向一个结构体,也可以创建指向结构的指针。
结构体指针
var struct_pointer *Books
以上定义的指针变量可以存储结构体变量的地址。查看结构体变量地址,可以将 & 符号放置于结构体变量前
struct_pointer = &Book1;
使用结构体指针访问结构体成员,使用 “.” 操作符
struct_pointer.title;
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int
}func main() {var Book1 Books /* Declare Book1 of type Book */var Book2 Books /* Declare Book2 of type Book *//* book 1 描述 */Book1.title = "Go 语言"Book1.author = "www.runoob.com"Book1.subject = "Go 语言教程"Book1.book_id = 6495407/* book 2 描述 */Book2.title = "Python 教程"Book2.author = "www.runoob.com"Book2.subject = "Python 语言教程"Book2.book_id = 6495700/* 打印 Book1 信息 */printBook(&Book1)/* 打印 Book2 信息 */printBook(&Book2)
}
func printBook( book *Books ) {fmt.Printf( "Book title : %s\n", book.title);fmt.Printf( "Book author : %s\n", book.author);fmt.Printf( "Book subject : %s\n", book.subject);fmt.Printf( "Book book_id : %d\n", book.book_id);
}
运行结果:
Book title : Go 语言
Book author : www.runoob.com
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : www.runoob.com
Book subject : Python 语言教程
Book book_id : 6495700
结构体实例化也可以是这样的
package mainimport "fmt"type Books struct {
}func (s Books) String() string {return "data"
}
func main() {fmt.Printf("%v\n", Books{})
}
解释:
在Books
类型上定义了一个String
方法。这个方法具有一个接收者(s Books)
,表示它是Books
类型的一个实例方法。这个方法返回一个字符串"data"
。Books{}
创建了一个Books
类型的实例,然后通过%v
打印输出。由于Books
类型实现了String
方法,该方法将被调用,返回字符串"data"
。
结构体的匿名字段
结构体的匿名字段
可以用字段来创建结构,这些字段只包含一个没有字段名的类型。这些字段被称为匿名字段。
在类型中,使用不写字段名的方式,使用另一个类型
type Human struct {name stringage intweight int
}
type Student struct {Human // 匿名字段,那么默认Student就包含了Human的所有字段speciality string
}
func main() {// 我们初始化一个学生mark := Student{Human{"Mark", 25, 120}, "Computer Science"}// 我们访问相应的字段fmt.Println("His name is ", mark.name)fmt.Println("His age is ", mark.age)fmt.Println("His weight is ", mark.weight)fmt.Println("His speciality is ", mark.speciality)// 修改对应的备注信息mark.speciality = "AI"fmt.Println("Mark changed his speciality")fmt.Println("His speciality is ", mark.speciality)// 修改他的年龄信息fmt.Println("Mark become old")mark.age = 46fmt.Println("His age is", mark.age)// 修改他的体重信息fmt.Println("Mark is not an athlet anymore")mark.weight += 60fmt.Println("His weight is", mark.weight)
}
输出:
His name is Mark
His age is 25
His weight is 120
His speciality is Computer Science
Mark changed his speciality
His speciality is AI
Mark become old
His age is 46
Mark is not an athlet anymore
His weight is 180
可以使用"."的方式进行调用匿名字段中的属性值
实际就是字段的继承
其中可以将匿名字段理解为字段名和字段类型都是同一个
基于上面的理解,所以可以
mark.Human = Human{"Marcus", 55, 220}
和mark.Human.age = 46
若存在匿名字段中的字段与非匿名字段名字相同,则最外层的优先访问,就近原则
通过匿名访问和修改字段相当的有用,但是不仅仅是struct字段哦,所有的内置类型和自定义类型都是可以作为匿名字段的。
结构体嵌套
嵌套的结构体
一个结构体可能包含一个字段,而这个字段反过来就是一个结构体。这些结构被称为嵌套结构。
示例代码:
package mainimport ( "fmt"
)type Address struct { city, state string
}
type Person struct { name stringage intaddress Address
}func main() { var p Personp.name = "Naveen"p.age = 50p.address = Address {city: "Chicago",state: "Illinois",}fmt.Println("Name:", p.name)fmt.Println("Age:",p.age)fmt.Println("City:",p.address.city)fmt.Println("State:",p.address.state)
}
提升字段
在结构体中属于匿名结构体的字段称为提升字段,因为它们可以被访问,就好像它们属于拥有匿名结构字段的结构一样。理解这个定义是相当复杂的。
示例代码:
package mainimport ( "fmt"
)type Address struct { city, state string
}
type Person struct { name stringage intaddress Address
}func main() { var p Personp.name = "Naveen"p.age = 50p.Address = Address{city: "Chicago",state: "Illinois",}fmt.Println("Name:", p.name)fmt.Println("Age:", p.age)fmt.Println("City:", p.city) //city is promoted fieldfmt.Println("State:", p.state) //state is promoted field
}
运行结果
Name: Naveen
Age: 50
City: Chicago
State: Illinois
导出结构体和字段
如果结构体类型以大写字母开头,那么它是一个导出类型,可以从其他包访问它。类似地,如果结构体的字段以大写开头,则可以从其他包访问它们。
示例代码:
1.在computer目录下,创建文件spec.go
package computertype Spec struct { //exported struct Maker string //exported fieldmodel string //unexported fieldPrice int //exported field
}
2.创建main.go 文件
package mainimport "structs/computer"
import "fmt"func main() { var spec computer.Specspec.Maker = "apple"spec.Price = 50000fmt.Println("Spec:", spec)
}
输出:
Spec: {apple 50000}
结构体比较
结构体是值类型,如果每个字段具有可比性,则是可比较的。如果它们对应的字段相等,则认为两个结构体变量是相等的。
示例代码:
package mainimport ( "fmt"
)type name struct { firstName stringlastName string
}func main() { name1 := name{"Steve", "Jobs"}name2 := name{"Steve", "Jobs"}if name1 == name2 {fmt.Println("name1 and name2 are equal")} else {fmt.Println("name1 and name2 are not equal")}name3 := name{firstName:"Steve", lastName:"Jobs"}name4 := name{}name4.firstName = "Steve"if name3 == name4 {fmt.Println("name3 and name4 are equal")} else {fmt.Println("name3 and name4 are not equal")}
}
运行结果
name1 and name2 are equal
name3 and name4 are not equal
如果结构变量包含的字段是不可比较的,那么结构变量是不可比较的
示例代码:
package mainimport ( "fmt"
)type image struct { data map[int]int
}func main() { image1 := image{data: map[int]int{0: 155,}}image2 := image{data: map[int]int{0: 155,}}if image1 == image2 {fmt.Println("image1 and image2 are equal")}
}
invalid operation: image1 == image2 (struct containing map[int]int cannot be compared)
结构体作为函数的参数
结构体作为函数参数使用
ackage mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int
}func main() {var Book1 Books /* 声明 Book1 为 Books 类型 */var Book2 Books /* 声明 Book2 为 Books 类型 *//* book 1 描述 */Book1.title = "Go 语言"Book1.author = "www.runoob.com"Book1.subject = "Go 语言教程"Book1.book_id = 6495407/* book 2 描述 */Book2.title = "Python 教程"Book2.author = "www.runoob.com"Book2.subject = "Python 语言教程"Book2.book_id = 6495700/* 打印 Book1 信息 */printBook(Book1)/* 打印 Book2 信息 */printBook(Book2)
}func printBook( book Books ) {fmt.Printf( "Book title : %s\n", book.title);fmt.Printf( "Book author : %s\n", book.author);fmt.Printf( "Book subject : %s\n", book.subject);fmt.Printf( "Book book_id : %d\n", book.book_id);
}
结构体方法
package mainimport "fmt"type user struct {name stringpassword string
}func (u user) checkPassword(password string) bool {return u.password == password
}func (u *user) resetPassword(password string) {u.password = password
}func main() {a := user{name: "wang", password: "1024"}a.resetPassword("2048")fmt.Println(a.checkPassword("2048")) // true
}
make、new操作
make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。
内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:new返回指针
内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始化了内部的数据结构,填充适当的值。make返回初始化后的(非零)值。
new
用于任何类型的内存分配,返回的是类型的指针,指向新分配的零值内存空间。make
仅用于切片、映射和通道的内存分配,返回的是经过初始化后的非零值。
指针与结构体举例
package mainimport "fmt"type user struct {name stringpassword string
}func main() {a := user{name: "wang", password: "1024"}b := user{"wang", "1024"}c := user{name: "wang"}c.password = "1024"var d userd.name = "wang"d.password = "1024"fmt.Println(a, b, c, d) // {wang 1024} {wang 1024} {wang 1024} {wang 1024}fmt.Println(checkPassword(a, "haha")) // falsefmt.Println(checkPassword2(&a, "haha")) // false
}func checkPassword(u user, password string) bool {return u.password == password
}func checkPassword2(u *user, password string) bool {return u.password == password
}
输出:
{wang 1024} {wang 1024} {wang 1024} {wang 1024}
false
false
方法
什么是方法
Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。
方法只是一个函数,它带有一个特殊的接收器类型,它是在func关键字和方法名之间编写的。接收器可以是struct类型或非struct类型。接收方可以在方法内部访问。
方法的语法
定义方法的语法
func (t Type) methodName(parameter list)(return list) {}
func funcName(parameter list)(return list){}
示例代码:
package mainimport ( "fmt"
)type Employee struct { name stringsalary intcurrency string
}/*displaySalary() method has Employee as the receiver type
*/
func (e Employee) displaySalary() { fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}func main() { emp1 := Employee {name: "Sam Adolf",salary: 5000,currency: "$",}emp1.displaySalary() //Calling displaySalary() method of Employee type
}
可以定义相同的方法名
示例代码:
package mainimport ("fmt""math"
)type Rectangle struct {width, height float64
}
type Circle struct {radius float64
}func (r Rectangle) area() float64 {return r.width * r.height
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) area() float64 {return c.radius * c.radius * math.Pi
}
func main() {r1 := Rectangle{12, 2}r2 := Rectangle{9, 4}c1 := Circle{10}c2 := Circle{25}fmt.Println("Area of r1 is: ", r1.area())fmt.Println("Area of r2 is: ", r2.area())fmt.Println("Area of c1 is: ", c1.area())fmt.Println("Area of c2 is: ", c2.area())
}
运行结果
Area of r1 is: 24
Area of r2 is: 36
Area of c1 is: 314.1592653589793
Area of c2 is: 1963.4954084936207
- 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样
- method里面可以访问接收者的字段
- 调用method通过.访问,就像struct里面访问字段一样
方法和函数
既然我们已经有了函数,为什么还要使用方法?
示例代码:
package mainimport ( "fmt"
)type Employee struct { name stringsalary intcurrency string
}/*displaySalary() method converted to function with Employee as parameter
*/
func displaySalary(e Employee) { fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}func main() { emp1 := Employee{name: "Sam Adolf",salary: 5000,currency: "$",}displaySalary(emp1)
}
在上面的程序中,displaySalary方法被转换为一个函数,而Employee struct作为参数传递给它。这个程序也产生了相同的输出:Salary of Sam Adolf is $5000.。
为什么我们可以用函数来写相同的程序呢?有以下几个原因
- Go不是一种纯粹面向对象的编程语言,它不支持类。因此,类型的方法是一种实现类似于类的行为的方法。
- 相同名称的方法可以在不同的类型上定义,而具有相同名称的函数是不允许的。假设我们有一个正方形和圆形的结构。可以在正方形和圆形上定义一个名为Area的方法。
变量作用域
作用域为已声明标识符所表示的常量、类型、变量、函数或包在源代码中的作用范围。
Go 语言中变量可以在三个地方声明:
- 函数内定义的变量称为局部变量
- 函数外定义的变量称为全局变量
- 函数定义中的变量称为形式参数
局部变量
在函数体内声明的变量称之为局部变量,它们的作用域只在函数体内,参数和返回值变量也是局部变量。
全局变量
在函数体外声明的变量称之为全局变量,首字母大写全局变量可以在整个包甚至外部包(被导出后)使用。
package mainimport "fmt"/* 声明全局变量 */
var g intfunc main() {/* 声明局部变量 */var a, b int/* 初始化参数 */a = 10b = 20g = a + bfmt.Printf("结果: a = %d, b = %d and g = %d\n", a, b, g)
}
结果
结果: a = 10, b = 20 and g = 30
形式参数
形式参数会作为函数的局部变量来使用
指针作为接收者
若不是以指针作为接收者,实际只是获取了一个copy,而不能真正改变接收者的中的数据
func (b *Box) SetColor(c Color) {b.color = c
}
示例代码
package mainimport ("fmt"
)type Rectangle struct {width, height int
}func (r *Rectangle) setVal() {r.height = 20
}func main() {p := Rectangle{1, 2}s := pp.setVal()fmt.Println(p.height, s.height)
}
结果
20 2
如果没有那个*,则值就是2 2
method继承
method是可以继承的,如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method
package mainimport "fmt"type Human struct {name stringage intphone string
}
type Student struct {Human //匿名字段school string
}
type Employee struct {Human //匿名字段company string
}func (h *Human) SayHi() {fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}mark.SayHi()sam.SayHi()
}
运行结果:
Hi, I am Mark you can call me on 222-222-YYYY
Hi, I am Sam you can call me on 111-888-XXXX
method重写
package mainimport "fmt"type Human struct {name stringage intphone string
}
type Student struct {Human //匿名字段school string
}
type Employee struct {Human //匿名字段company string
}//Human定义method
func (h *Human) SayHi() {fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}//Employee的method重写Human的method
func (e *Employee) SayHi() {fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,e.company, e.phone) //Yes you can split into 2 lines here.
}
func main() {mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}mark.SayHi()sam.SayHi()
}
运行结果:
Hi, I am Mark you can call me on 222-222-YYYY
Hi, I am Sam, I work at Golang Inc. Call me on 111-888-XXXX
- 方法是可以继承和重写的
- 存在继承关系时,按照就近原则,进行调用
接口
什么是接口
面向对象世界中的接口的一般定义是“接口定义对象的行为”。它表示让指定对象应该做什么。实现这种行为的方法(实现细节)是针对对象的。
在Go中,接口是一组方法签名。当类型为接口中的所有方法提供定义时,它被称为实现接口。它与OOP非常相似。接口指定了类型应该具有的方法,类型决定了如何实现这些方法。
它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口
接口定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了该接口。
接口的定义语法
定义接口
/* 定义接口 */
type interface_name interface {method_name1 [return_type]method_name2 [return_type]method_name3 [return_type]...method_namen [return_type]
}/* 定义结构体 */
type struct_name struct {/* variables */
}/* 实现接口方法 */
func (struct_name_variable struct_name) method_name1() [return_type] {/* 方法实现 */
}
...
func (struct_name_variable struct_name) method_namen() [return_type] {/* 方法实现*/
}
示例代码:
package mainimport ("fmt"
)type Phone interface {call()
}type NokiaPhone struct {
}func (nokiaPhone NokiaPhone) call() {fmt.Println("I am Nokia, I can call you!")
}type IPhone struct {
}func (iPhone IPhone) call() {fmt.Println("I am iPhone, I can call you!")
}func main() {var phone Phonephone = new(NokiaPhone)phone.call()phone = new(IPhone)phone.call()}
由于NokiaPhone
结构体实现了Phone
接口中的方法,因此可以将其赋值给phone
。
运行结果:
I am Nokia, I can call you!
I am iPhone, I can call you!
- interface可以被任意的对象实现
- 一个对象可以实现任意多个interface
- 任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface
interface值
package mainimport "fmt"type Human struct {name stringage intphone string
}
type Student struct {Human //匿名字段school stringloan float32
}
type Employee struct {Human //匿名字段company stringmoney float32
} //Human实现Sayhi方法
func (h Human) SayHi() {fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
} //Human实现Sing方法
func (h Human) Sing(lyrics string) {fmt.Println("La la la la...", lyrics)
} //Employee重写Human的SayHi方法
func (e Employee) SayHi() {fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,e.company, e.phone) //Yes you can split into 2 lines here.
}// Interface Men被Human,Student和Employee实现
// 因为这三个类型都实现了这两个方法
type Men interface {SayHi()Sing(lyrics string)
}func main() {mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00}paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100}sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000}Tom := Employee{Human{"Sam", 36, "444-222-XXX"}, "Things Ltd.", 5000}//定义Men类型的变量ivar i Men//i能存储Studenti = mikefmt.Println("This is Mike, a Student:")i.SayHi()i.Sing("November rain")//i也能存储Employeei = Tomfmt.Println("This is Tom, an Employee:")i.SayHi()i.Sing("Born to be wild")//定义了slice Menfmt.Println("Let's use a slice of Men and see what happens")x := make([]Men, 3)//T这三个都是不同类型的元素,但是他们实现了interface同一个接口x[0], x[1], x[2] = paul, sam, mikefor _, value := range x {value.SayHi()}
}
运行结果:
This is Mike, a Student:
Hi, I am Mike you can call me on 222-222-XXX
La la la la... November rain
This is Tom, an Employee:
Hi, I am Sam, I work at Things Ltd.. Call me on 444-222-XXX
La la la la... Born to be wild
Let's use a slice of Men and see what happens
Hi, I am Paul you can call me on 111-222-XXX
Hi, I am Sam, I work at Golang Inc.. Call me on 444-222-XXX
Hi, I am Mike you can call me on 222-222-XXX
那么interface里面到底能存什么值呢?如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。例如上面例子中,我们定义了一个Men interface类型的变量m,那么m里面可以存Human、Student或者Employee值
当然,使用指针的方式,也是可以的
但是,接口对象不能调用实现对象的属性
interface函数参数
interface的变量可以持有任意实现该interface类型的对象,这给我们编写函数(包括method)提供了一些额外的思考,我们是不是可以通过定义interface参数,让函数接受各种类型的参数
嵌入interface
package mainimport "fmt"type Human interface {Len()
}
type Student interface {Human
}type Test struct {
}func (h *Test) Len() {fmt.Println("成功")
}
func main() {var s Students = new(Test)s.Len()
}
定义了一个Student
接口,它通过嵌套Human
接口,继承了Human
接口中的方法。在Test
结构体上定义了一个指针接收者的Len
方法。该方法实现了Human
接口中的Len
方法。通过s.Len()
调用了Test
结构体中实现的Len
方法,并输出"成功"。这样,通过接口的嵌套,我们可以在一个接口中组合多个其他接口,并且实现了嵌套接口的方法的结构体也被认为是实现了嵌套接口。这样可以实现接口的扩展和复用。
示例代码:
package testimport ("fmt"
)type Controller struct {M int32
}type Something interface {Get()Post()
}func (c *Controller) Get() {fmt.Print("GET")
}func (c *Controller) Post() {fmt.Print("POST")
}
package mainimport ("fmt""test"
)type T struct {test.Controller
}func (t *T) Get() {//new(test.Controller).Get()fmt.Print("T")
}
func (t *T) Post() {fmt.Print("T")
}
func main() {var something test.Somethingsomething = new(T)var t Tt.M = 1// t.Controller.M = 1something.Get()
}
Controller实现了所有的Something接口方法,当结构体T中调用Controller结构体的时候,T就相当于Java中的继承,T继承了Controller,因此,T可以不用重写所有的Something接口中的方法,因为父构造器已经实现了接口。
如果Controller没有实现Something接口方法,则T要调用Something中方法,就要实现其所有方法。
如果something = new(test.Controller)
则调用的是Controller中的Get方法,T可以使用Controller结构体中定义的变量
总之,接口对象不能调用接口实现对象的属性
补充关于闭包,递归,匿名函数,回调函数,strings包、strconv包的使用。
闭包
go语言支持函数式编程:
支持将一个函数作为另一个函数的参数,也支持将一个函数作为另一个函数的返回值。
闭包(closure):
一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量(外层函数中的参数,或者外层函数中直接定义的变量),并且该外层函数的返回值就是这个内层函数。这个内层函数和外层函数的局部变量,统称为闭包结构。
局部变量的生命周期会发生改变,正常的局部变量随着函数调用而创建,随着函数的结束而销毁。
但是闭包结构中的外层函数的局部变量并不会随着外层函数的结束而销毁,因为内层函数还要继续使用。
示例代码:
package mainimport "fmt"func main() {res1 := increment() //res1 = funfmt.Printf("%T\n", res1) //func() intfmt.Println(res1)v1 := res1()fmt.Println(v1) //1v2 := res1()fmt.Println(v2) //2fmt.Println(res1())fmt.Println(res1())fmt.Println(res1())fmt.Println(res1())res2 := increment()fmt.Println(res2)v3 := res2()fmt.Println(v3) //1fmt.Println(res2())fmt.Println(res1())
}func increment() func() int { //外层函数//1.定义了一个局部变量i := 0//2.定义了一个匿名函数,给变量自增并返回fun := func() int { //内层函数i++return i}//3.返回该匿名函数return fun
}
输出结果:
func() int
0xdbfd60
1
2
3
4
5
6
0xdbfd40
1
2
7
递归
递归函数(recursion):一个函数自己调用自己,就叫做递归函数。
一个函数自己调用自己,就叫做递归调用,一个递归函数一定要有出口,否则会陷入死循环。
示例代码:
package mainimport "fmt"func main() {sum := getSum(5)fmt.Println(sum)res := getFibonacci(12)fmt.Println(res)
}
func getFibonacci(n int) int {if n == 1 || n == 2 {return 1}return getFibonacci(n-1) + getFibonacci(n-2)
}func getSum(n int) int {fmt.Println("**********")if n == 1 {return 1}return getSum(n-1) + n
}
运行结果:
**********
**********
**********
**********
**********
15
144
匿名函数
匿名:没有名字
匿名函数:没有名字的函数。
定义一个匿名函数,直接进行调用。通常只能使用一次。也可以使用匿名函数赋值给某个函数变量,那么就可以调用多次了。
匿名函数:
Go语言是支持函数式编程:
将匿名函数作为另一个函数的参数,回调函数
将匿名函数作为另一个函数的返回值,可以形成闭包结构。
示例代码:
package mainimport "fmt"func main() {fun1()fun1()fun2 := fun1fun2()//匿名函数func() {fmt.Println("我是一个匿名函数。。")}()fun3 := func() {fmt.Println("我也是一个匿名函数。。")}fun3()fun3()//定义带参数的匿名函数func(a, b int) {fmt.Println(a, b)}(1, 2)//定义带返回值的匿名函数res1 := func(a, b int) int {return a + b}(10, 20) //匿名函数调用了,将执行结果给res1fmt.Println(res1)res2 := func(a, b int) int {return a + b} //将匿名函数的值,赋值给res2fmt.Println(res2)fmt.Println(res2(100, 200))
}func fun1() {fmt.Println("我是fun1()函数。。")
}
输出结果:
我是fun1()函数。。
我是fun1()函数。。
我是fun1()函数。。
我是一个匿名函数。。
我也是一个匿名函数。。
我也是一个匿名函数。。
1 2
30
0x63e160
300
回调函数
高阶函数:
根据go语言的数据类型的特点,可以将一个函数作为另一个函数的参数。
fun1(),fun2()
将fun1函数作为了fun2这个函数的参数。
fun2函数:就叫高阶函数
接收了一个函数作为参数的函数,高阶函数
fun1函数:回调函数
作为另一个函数的参数的函数,叫做回调函数。
示例代码:
package mainimport "fmt"func main() {//设计一个函数,用于求两个整数的加减乘除运算fmt.Printf("%T\n", add) //func(int, int) intfmt.Printf("%T\n", oper) //func(int, int, func(int, int) int) intres1 := add(1, 2)fmt.Println(res1)res2 := oper(10, 20, add)fmt.Println(res2)res3 := oper(5, 2, sub)fmt.Println(res3)fun1 := func(a, b int) int {return a * b}res4 := oper(10, 4, fun1)fmt.Println(res4)res5 := oper(100, 8, func(a, b int) int {if b == 0 {fmt.Println("除数不能为零")return 0}return a / b})fmt.Println(res5)}
func oper(a, b int, fun func(int, int) int) int {fmt.Println(a, b, fun) //打印3个参数res := fun(a, b)return res
}// 加法运算
func add(a, b int) int {return a + b
}// 减法
func sub(a, b int) int {return a - b
}
输出结果:
func(int, int) int
func(int, int, func(int, int) int) int
3
10 20 0xb8fc20
30
5 2 0xb8fc40
3
10 4 0xb8fc60
40
100 8 0xb8fc80
12
字符串
Go中的字符串是一个字节的切片。
可以通过将其内容封装在“”中来创建字符串。Go中的字符串是Unicode兼容的,并且是UTF-8编码的。
字符串是一些字节的集合。理解为一个字符的序列。
每个字符都有固定的位置(索引,下标,index:从0开始,到长度减1)
示例代码:
package mainimport "fmt"func main() {//1.定义字符串s1 := "hello中国"s2 := `hello world`fmt.Println(s1)fmt.Println(s2)//2.字符串的长度:返回的是字节的个数fmt.Println(len(s1))fmt.Println(len(s2))//3.获取某个字节fmt.Println(s2[0]) //获取字符串中的第一个字节a := 'h'b := 104fmt.Printf("%c,%c,%c\n", s2[0], a, b)//4.字符串的遍历for i := 0; i < len(s2); i++ {//fmt.Println(s2[i])fmt.Printf("%c\t", s2[i])}fmt.Println()//for rangefor _, v := range s2 {//fmt.Println(i,v)fmt.Printf("%c", v)}fmt.Println()//5.字符串是字节的集合slice1 := []byte{65, 66, 67, 68, 69}s3 := string(slice1) //根据一个字节切片,构建字符串fmt.Println(s3)s4 := "abcdef"slice2 := []byte(s4) //根据字符串,获取对应的字节切片fmt.Println(slice2)//6.字符串不能修改fmt.Println(s4)//s4[2] = 'B'
}
输出结果:
hello中国
hello world
11
11
104
h,h,h
h e l l o w o r l d
hello world
ABCDE
[97 98 99 100 101 102]
abcdef
strings包
示例代码:
package mainimport ("fmt""strings"
)func main() {s1 := "helloworld"//1.是否包含指定的内容-->boolfmt.Println(strings.Contains(s1, "abc"))//2.是否包含chars中任意的一个字符即可fmt.Println(strings.ContainsAny(s1, "abcd"))//3.统计substr在s中出现的次数fmt.Println(strings.Count(s1, "lloo"))//4.以xxx前缀开头,以xxx后缀结尾s2 := "20230525课堂笔记.txt"if strings.HasPrefix(s2, "202305") {fmt.Println("23年5月的文件。。")}if strings.HasSuffix(s2, ".txt") {fmt.Println("文本文档。。")}//索引//helloworldfmt.Println(strings.Index(s1, "lloo")) //查找substr在s中的位置,如果不存在就返回-1fmt.Println(strings.IndexAny(s1, "abcdh")) //查找chars中任意的一个字符,出现在s中的位置fmt.Println(strings.LastIndex(s1, "l")) //查找substr在s中最后一次出现的位置//字符串的拼接ss1 := []string{"abc", "world", "hello", "ruby"}s3 := strings.Join(ss1, "-")fmt.Println(s3)//切割s4 := "123,4563,aaa,49595,45"ss2 := strings.Split(s4, ",") //数组//fmt.Println(ss2)for i := 0; i < len(ss2); i++ {fmt.Println(ss2[i])}//重复,自己拼接自己count次s5 := strings.Repeat("hello", 5)fmt.Println(s5)//替换//helloworlds6 := strings.Replace(s1, "l", "*", -1)fmt.Println(s6)//fmt.Println(strings.Repeat("hello",5))s7 := "heLLo WOrlD**123.."fmt.Println(strings.ToLower(s7))fmt.Println(strings.ToUpper(s7))/*截取子串:substring(start,end)-->substrstr[start:end]-->substr包含start,不包含end下标*/fmt.Println(s1)s8 := s1[:5]fmt.Println(s8)fmt.Println(s1[5:])
}
输出结果:
false
true
0
23年5月的文件。。
文本文档。。
-1
0
8
abc-world-hello-ruby
123
4563
aaa
49595
45
hellohellohellohellohello
he**owor*d
hello world**123..
HELLO WORLD**123..
helloworld
hello
world
strconv包
示例代码:
package mainimport ("fmt""strconv"
)func main() {//1.bool类型s1 := "true"b1, err := strconv.ParseBool(s1)if err != nil {fmt.Println(err)return}fmt.Printf("%T,%t\n", b1, b1) //%t是bool类型ss1 := strconv.FormatBool(b1)fmt.Printf("%T,%s\n", ss1, ss1)//2.整数s2 := "100"i2, err := strconv.ParseInt(s2, 2, 64)if err != nil {fmt.Println(err)return}fmt.Printf("%T,%d\n", i2, i2)ss2 := strconv.FormatInt(i2, 10)fmt.Printf("%T,%s\n", ss2, ss2)//itoa(),atoi()i3, err := strconv.Atoi("-42") //转为int类型fmt.Printf("%T,%d\n", i3, i3)ss3 := strconv.Itoa(-42)fmt.Printf("%T,%s\n", ss3, ss3)
}
输出结果:
bool,true
string,true
int64,4
string,4
int,-42
string,-42
“strconv"用于进行字符串和基本类型之间的转换。在main函数中,首先演示了布尔类型的转换。使用strconv.ParseBool函数将s1转换为bool类型的变量b1。如果转换过程中出现错误,则会打印错误信息并返回。否则,使用fmt.Printf函数输出b1的类型和值。接下来,使用strconv.FormatBool函数将b1转换为字符串类型的变量ss1,演示了整数类型的转换,使用strconv.ParseInt函数将s2按照二进制解析为int64类型的变量i2。使用strconv.FormatInt函数将i2按照十进制转换为字符串类型的变量ss2,使用strconv.Atoi函数将字符串”-42"转换为int类型的变量i3,使用strconv.Itoa函数将整数-42转换为字符串类型的变量ss3。
type关键字
type:用于类型定义和类型别名
- 类型定义:type 类型名 Type
- 类型别名:type 类型名 = Type
示例代码:
package mainimport ("fmt""strconv"
)func main() {var i1 myintvar i2 = 100 //inti1 = 200fmt.Println(i1, i2)var name mystrname = "王二狗"var s1 strings1 = "李小花"fmt.Println(name, s1)//i1 = i2 //cannot use i2 (type int) as type myint in assignment//name = s1 //cannot use s1 (type string) as type mystr in assignmentfmt.Printf("%T,%T,%T,%T\n", i1, i2, name, s1) //main.myint,int,main.mystr,stringfmt.Println("----------------------------------")res1 := fun1()fmt.Println(res1(10, 20))fmt.Println("----------------------------------")var i3 myint2i3 = 1000fmt.Println(i3)i3 = i2fmt.Println(i3)fmt.Printf("%T,%T,%T\n", i1, i2, i3) //main.myint,int,int}// 1.定义一个新的类型
type myint int
type mystr string// 2.定义函数类型
type myfun func(int, int) stringfunc fun1() myfun { //fun1()函数的返回值是myfun类型fun := func(a, b int) string {s := strconv.Itoa(a) + strconv.Itoa(b)return s}return fun
}// 3.类型别名
type myint2 = int //不是重新定义新的数据类型,只是给int起别名,和int可以通用
输出结果:
200 100
王二狗 李小花
main.myint,int,main.mystr,string
----------------------------------
1020
----------------------------------
1000
100
main.myint,int,int
尽管类型别名可以方便地使用现有类型,但它们不会创建一个新的类型。因此,无法在类型别名上定义新方法。只有在本地类型上才能定义新方法。
错误代码:
package mainimport "time"func main() {}type MyDuration = time.Durationfunc (m MyDuration) SimpleSet() { //cannot define new methods on non-local type time.Duration}
要么在time包下定义新方法,要么建一个新的命名类型,而不是使用类型别名。
修正:
package mainimport "time"type MyDuration time.Durationfunc (m MyDuration) SimpleSet() {// 在这里实现SimpleSet方法
}func main() {// 在这里使用MyDuration类型
}
在结构体成员嵌入时使用别名:
package mainimport "fmt"type Person struct {name string
}func (p Person) show() {fmt.Println("Person--->", p.name)
}// People 类型别名
type People = Personfunc (p People) show2() {fmt.Println("People--->", p.name)
}type Student struct {//嵌入两个结构体PersonPeople
}func main() {var s Student//s.name = "王二狗" //ambiguous selector s.names.Person.name = "王二狗"//s.show() //ambiguous selector s.shows.Person.show()fmt.Printf("%T,%T\n", s.Person, s.People) //main.Person,main.Persons.People.name = "李小花"s.People.show2()s.Person.show()
}
输出结果:
Person---> 王二狗
main.Person,main.Person
People---> 李小花
Person---> 王二狗
错误处理
在实际工程项目中,我们希望通过程序的错误信息快速定位问题,但是又不喜欢错误处理代码写的冗余而又啰嗦。Go语言没有提供像Java、c#语言中的try…catch异常处理方式,而是通过函数返回值逐层往上抛。这种设计,鼓励工程师在代码中显式的检查错误,而非忽略错误,好处就是避免漏掉本应处理的错误。但是带来一个弊端,让代码啰嗦。
什么是错误
错误是什么?
错误指的是可能出现问题的地方出现了问题。比如打开一个文件时失败,这种情况在人们的意料之中。
而异常指的是不应该出现问题的地方出现了问题。比如引用了空指针,这种情况在人们的意料之外。可见,错误是业务过程的一部分,而异常不是。
Go中的错误也是一种类型。错误用内置的error类型表示。就像其他类型的,如int,float64。错误值可以存储在变量中,从函数中返回,等等。
演示错误
让我们从一个示例程序开始,这个程序尝试打开一个不存在的文件。
示例代码:
package mainimport ("fmt""os"
)func main() {f, err := os.Open("test.txt")if err != nil {//log.Fatal(err)fmt.Println(err) //open test.txt: no such file or directoryif ins, ok := err.(*os.PathError); ok {fmt.Println("1.Op:", ins.Op)fmt.Println("2.Path:", ins.Path)fmt.Println("3.Err:", ins.Err)}return}fmt.Println(f.Name(), "打开文件成功。。")}
输出结果:
open test.txt: The system cannot find the file specified.
1.Op: open
2.Path: test.txt
3.Err: The system cannot find the file specified.
这行代码尝试将错误值 err
转换为 *os.PathError
类型,并将结果存储在变量 ins
中。这是一个类型断言的示例,它检查错误类型是否是 *os.PathError
,并且返回一个布尔值 ok
表示是否成功进行了类型转换。如果类型断言成功,这些代码将打印 *os.PathError
类型的特定字段信息。Op
字段表示操作类型,Path
字段表示操作的路径,Err
字段表示底层错误。
自定义函数返回错误
error:内置的数据类型,内置的接口
定义方法:Error() string
使用go语言提供好的包:
errors包下的函数:New(),创建一个error对象,fmt包下的Errorf()函数:
func Errorf(format string, a …interface{}) error
示例代码:
package mainimport ("errors""fmt"
)func main() {//1.创建一个error数据err1 := errors.New("自己创建玩的。。")fmt.Println(err1)fmt.Printf("%T\n", err1) //*errors.errorString//2.另一个创建error的方法err2 := fmt.Errorf("错误的信息码:%d", 100)fmt.Println(err2)fmt.Printf("%T\n", err2)fmt.Println("-----------------")err3 := checkAge(-30)if err3 != nil {fmt.Println(err3)return}fmt.Println("程序。。。go on。。。")
}// 设计一个函数:验证年龄是否合法,如果为负数,就返回一个error
func checkAge(age int) error {if age < 0 {//返回error对象//return errors.New("年龄不合法")err := fmt.Errorf("您给定的年龄是:%d,不合法", age)return err}fmt.Println("年龄是:", age)return nil
}
输出结果:
自己创建玩的。。
*errors.errorString
错误的信息码:100
*errors.errorString
-----------------
您给定的年龄是:-30,不合法
错误的类型表示
if ins, ok := err.(*os.PathError); ok {fmt.Println("1.Op:", ins.Op)fmt.Println("2.Path:", ins.Path)fmt.Println("3.Err:", ins.Err)}
2.获得更多信息的第二种方法是断言底层类型,并通过调用struct类型的方法获取更多信息。
示例代码:
package mainimport ("fmt""net"
)func main() {addr, err := net.LookupHost("www.baidu.com")fmt.Println(err)if ins, ok := err.(*net.DNSError); ok {if ins.Timeout() {fmt.Println("操作超时。。")} else if ins.Temporary() {fmt.Println("临时性错误。。")} else {fmt.Println("通常错误。。")}}fmt.Println(addr)
}
输出结果:
<nil>
[182.61.200.6 182.61.200.7]
3.直接比较
获得更多关于错误的详细信息的第三种方法是直接与类型错误的变量进行比较。让我们通过一个例子来理解这个问题。
filepath包的Glob函数用于返回与模式匹配的所有文件的名称。当模式出现错误时,该函数将返回一个错误ErrBadPattern。
在filepath包中定义了ErrBadPattern,如下所述:
var ErrBadPattern = errors.New("syntax error in pattern")
errors.New()用于创建新的错误。
当模式出现错误时,由Glob函数返回ErrBadPattern。
让我们写一个小程序来检查这个错误:
package mainimport ("fmt""path/filepath"
)func main() {files, err := filepath.Glob("[")if err != nil && err == filepath.ErrBadPattern {fmt.Println(err) //syntax error in patternreturn}fmt.Println("files:", files)
}
输出结果:
syntax error in pattern
记住永远不要忽略一个错误。忽视错误会招致麻烦。
自定义错误
示例代码:
package mainimport ("fmt""math"
)func main() {radius := -3.0area, err := circleArea(radius)if err != nil {fmt.Println(err)if err, ok := err.(*areaError); ok {fmt.Printf("半径是:%.2f\n", err.radius)}return}fmt.Println("圆形的面积是:", area)}// 1.定义一个结构体,表示错误的类型
type areaError struct {msg stringradius float64
}// 2.实现error接口,就是实现Error()方法
func (e *areaError) Error() string {return fmt.Sprintf("error:半径,%.2f,%s", e.radius, e.msg)
}func circleArea(radius float64) (float64, error) {if radius < 0 {return 0, &areaError{"半径是非法的", radius}}return math.Pi * radius * radius, nil
}
输出结果:
error:半径,-3.00,半径是非法的
半径是:-3.00
示例代码:
package mainimport "fmt"func main() {length, width := -6.7, -9.1area, err := rectArea(length, width)if err != nil {fmt.Println(err)if err, ok := err.(*areaError); ok {if err.legnthNegative() {fmt.Printf("error:长度,%.2f,小于零\n", err.length)}if err.widthNegative() {fmt.Printf("error:宽度,%.2f,小于零\n", err.width)}}return}fmt.Println("矩形的面积是:", area)
}type areaError struct {msg string //错误的描述length float64 //发生错误的时候,矩形的长度width float64 //发生错误的时候,矩形的宽度
}func (e *areaError) Error() string {return e.msg
}func (e *areaError) legnthNegative() bool {return e.length < 0
}func (e *areaError) widthNegative() bool {return e.width < 0
}func rectArea(length, width float64) (float64, error) {msg := ""if length < 0 {msg = "长度小于零"}if width < 0 {if msg == "" {msg = "宽度小于零"} else {msg += ",宽度也小于零"}}if msg != "" {return 0, &areaError{msg, length, width}}return length * width, nil
}
输出结果:
长度小于零,宽度也小于零
error:长度,-6.70,小于零
error:宽度,-9.10,小于零
panic和recover
Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。
一直等到包含defer语句的函数执行完毕时,延迟函数(defer后的函数)才会被执行,而不管包含defer语句的函数是通过return的正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。
当程序运行时,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。如果一路在延迟函数中没有recover函数的调用,则会到达该协程的起点,该协程结束,然后终止其他所有协程,包括主协程(类似于C语言中的主线程,该协程ID为1)。
panic:
-
内建函数
-
假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
-
返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行,这里的defer有点类似 try-catch-finally中的finally
-
直到goroutine整个退出,并报告错误
recover:
-
内建函数
-
用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
-
一般的调用建议
a).在defer函数中,通过recover来终止一个gojroutine的panicking过程,从而恢复正常代码的执行
b).可以获取通过panic传递的error
简单来讲: go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。
示例代码:
package mainimport "fmt"func main() {defer func() {if msg := recover(); msg != nil {fmt.Println(msg, "程序恢复啦。。。")}}()funA()defer myprint("defer main:3.....")funB()defer myprint("defer main:4.....")fmt.Println("main..over。。。。")}
func myprint(s string) {fmt.Println(s)
}func funA() {fmt.Println("我是一个函数funA()....")
}func funB() { //外围函数fmt.Println("我是函数funB()...")defer myprint("defer funB():1.....")for i := 1; i <= 10; i++ {fmt.Println("i:", i)if i == 5 {//让程序中断panic("funB函数,恐慌了")}} //当外围函数的代码中发生了运行恐慌,只有其中所有的已经defer的函数全部都执行完毕后,该运行恐慌才会真正被扩展至调用处。defer myprint("defer funB():2.....")
}
输出结果:
我是一个函数funA()....
我是函数funB()...
i: 1
i: 2
i: 3
i: 4
i: 5
defer funB():1.....
defer main:3.....
funB函数,恐慌了 程序恢复啦。。。
由于恐慌发生在循环内部,之后的语句将不会执行。然而,之前已经被defer
关键字延迟执行的myprint
函数仍然会执行。因此,"defer funB():1…“会被输出。funB函数的执行完毕后,恐慌会被传播到调用处,也就是main
函数中的匿名函数。在这个匿名函数中,我们通过recover
函数检测到了恐慌,所以会输出恐慌信息:“funB函数,恐慌了”,并打印"程序恢复啦。。。”。
错误和异常从Golang机制上讲,就是error和panic的区别。很多其他语言也一样,
比C++/Java,没有error有errno,没有panic但有throw。
Golang错误和异常是可以互相转换的:
1.错误转异常,比如程序逻辑上尝试请求某个URL,最多尝试三次,尝试三次的过程中请求失败是错误,尝试完第三次还不成功的话,失败就被提升为异常了。
2.异常转错误,比如panic触发的异常被recover恢复后,将返回值中error类型的变量进行赋值,以便上层函数继续走错误处理流程。
什么情况下用错误表达,什么情况下用异常表达,就得有一套规则,否则很容易出现一切皆错误或一切皆异常的情况。
以下给出异常处理的作用域(场景)∶
1.空指针引用
2.下标越界
3.除数为0
4.不应该出现的分支,比如default
5.输入不应该引起函数错误
其他场景我们使用错误处理,这使得我们的函数接口很精炼。对于异常,我们可以选择在一个合适的上游去recover,并打印堆栈信息,使得部署后的程序不会终止。
包的使用
Go语言使用包(package)这种语法元素来组织源码,所有语法可见性均定义在package这个级别,与Java 、python等语言相比,这算不上什么创新,但与C传统的include相比,则是显得“先进”了许多。
Go 语言的源码复用建立在包(package)基础之上。包通过 package, import, GOPATH 操作完成。
main包
Go 语言的入口 main() 函数所在的包(package)叫 main,main 包想要引用别的代码,需要import导入!
package
src 目录是以代码包的形式组织并保存 Go 源码文件的。每个代码包都和 src 目录下的文件夹一一对应。每个子目录都是一个代码包。
代码包包名和文件目录名,不要求一致。比如文件目录叫 hello,但是代码包包名可以声明为 “main”,但是同一个目录下的源码文件第一行声明的所属包,必须一致!
同一个目录下的所有.go文件的第一行添加 包定义,以标记该文件归属的包,演示语法:
package 包名
包需要满足:
- 一个目录下的同级文件归属一个包。也就是说,在同一个包下面的所有文件的package名,都是一样的。
- 在同一个包下面的文件
package
名都建议设为是该目录名,但也可以不是。也就是说,包名可以与其目录不同名。 - 包名为 main 的包为应用程序的入口包,其他包不能使用。
在同一个包下面的文件属于同一个工程文件,不用
import
包,可以直接使用
包可以嵌套定义,对应的就是嵌套目录,但包名应该与所在的目录一致。
包中,通过标识符首字母是否大写,来确定是否可以被导出。首字母大写才可以被导出,视为 public 公共的资源。
import
要引用其他包,可以使用 import 关键字,可以单个导入或者批量导入,语法演示:
A:通常导入
// 单个导入
import "package"
// 批量导入
import ("package1""package2")
B:点操作
我们有时候会看到如下的方式导入包
import(. "fmt"
)
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名。
示例代码:
package mainimport (. "fmt"
)type S struct{}func (s S) p(n int) S {Print(n)return s
}
func main() {var s Sdefer s.p(1).p(2)Print(3)
}
C:起别名
别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字。导入时,可以为包定义别名,语法演示:
import (p1 "package1"p2 "package2")
// 使用时:别名操作,调用包函数时前缀变成了我们的前缀
p1.Method()
D:_操作
如果仅仅需要导入包时执行初始化操作,并不需要使用包内的其他函数,常量等资源。则可以在导入包时,匿名导入。
这个操作经常是让很多人费解的一个操作符,请看下面这个import:
package mainimport ("database/sql""fmt"_ "github.com/go-sql-driver/mysql"
)func main() {db, err := sql.Open("mysql", "root:@tcp(127.0.0.1:3306)/flask_meeting?charset=utf8mb4")if err != nil {fmt.Println("错误信息", err)return}fmt.Println("连接成功", db)
}
_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。也就是说,使用下划线作为包的别名,会仅仅执行init()。
导入的包的路径名,可以是相对路径也可以是绝对路径,推荐使用绝对路径(起始于工程根目录)。
GOPATH环境变量
import导入时,会从GO的安装目录(也就是GOROOT环境变量设置的目录)和GOPATH环境变量设置的目录中,检索 src/package 来导入包。如果不存在,则导入失败。
GOROOT,就是GO内置的包所在的位置。
GOPATH,就是我们自己定义的包的位置。
通常我们在开发Go项目时,调试或者编译构建时,需要设置GOPATH指向我们的项目目录,目录中的src目录中的包就可以被导入了。
init() 包初始化
下面我们详细的来介绍一下这两个函数:init()、main() 是 go 语言中的保留函数。我们可以在源码中,定义 init() 函数。此函数会在包被导入时执行,例如如果是在 main 中导入包,包中存在 init(),那么 init() 中的代码会在 main() 函数执行前执行,用于初始化包所需要的特定资料。例如:
包源码:
//src/userPackage/tool.gopackage userPackage
import "fmt"
func init() {fmt.Println("tool init")
}
主函数源码:
//src/main.gopackage main
import ("userPackage")
func main() {fmt.Println("main run")// 使用userPackageuserPackage.SomeFunc()
}
执行时,会先输出 “tool init”,再输出 “main run”。
下面我们详细的来介绍一下init()、main() 这两个函数。在 go 语言中的区别如下:
相同点:
两个函数在定义时不能有任何的参数和返回值。
该函数只能由 go 程序自动调用,不可以被引用。
不同点:
init 可以应用于任意包中,且可以重复定义多个。
main 函数只能用于 main 包中,且只能定义一个。
两个函数的执行顺序:
在 main 包中的 go 文件默认总是会被执行。
-
对同一个 go 文件的 init( ) 调用顺序是从上到下的。
-
对同一个 package 中的不同文件,将文件名按字符串进行“从小到大”排序,之后顺序调用各文件中的init()函数。
-
对于不同的 package,如果不相互依赖的话,按照 main 包中 import 的顺序调用其包中的 init() 函数。
-
如果 package 存在依赖,调用顺序为最后被依赖的最先被初始化,例如:导入顺序 main –> A –> B –> C,则初始化顺序为 C –> B –> A –> main,一次执行对应的 init 方法。main 包总是被最后一个初始化,因为它总是依赖别的包
避免出现循环 import,例如:A –> B –> C –> A。
一个包被其它多个包 import,但只能被初始化一次
管理外部包
go允许import不同代码库的代码。对于import要导入的外部的包,可以使用 go get 命令取下来放到GOPATH对应的目录中去。
举个例子,比如说我们想通过go语言连接mysql数据库,那么需要先下载mysql的数据包,打开终端并输入以下命令:
$ go get github.com/go-sql-driver/mysql
也就是说,对于go语言来讲,其实并不关心你的代码是内部还是外部的,总之都在GOPATH里,任何import包的路径都是从GOPATH开始的;唯一的区别,就是内部依赖的包是开发者自己写的,外部依赖的包是go get下来的。
最后
我们可以通过go install 来编译包文件。
我们知道一个非main包在编译后会生成一个.a文件(在临时目录下生成,除非使用go install安装到 G O R O O T 或 GOROOT或 GOROOT或GOPATH下,否则你看不到.a),用于后续可执行程序链接使用。
比如Go标准库中的包对应的源码部分路径在: G O R O O T / s r c ,而标准库中包编译后的 . a 文件路径在 GOROOT/src,而标准库中包编译后的.a文件路径在 GOROOT/src,而标准库中包编译后的.a文件路径在GOROOT/pkg/darwin_amd64下。
- pkg 目录是用来存放通过 go install 命令安装后的代码包的归档文件(.a 文件)。归档文件的名字就是代码包的名字。所有归档文件都会被存放到该目录下的平台相关目录中,即在
$GOPATH/pkg/$GOOS_$GOARCH
中,同样以代码包为组织形式。
time包
time包:
1年=365天,day
1天=24小时,hour
1小时=60分钟,minute
1分钟=60秒,second
1秒钟=1000毫秒,millisecond
1毫秒=1000微秒,microsecond–>μs
1微秒=1000纳秒,nanosecond–>ns
1纳秒=1000皮秒,picosecond–>ps
示例代码:
package mainimport ("fmt""math/rand""time"
)func main() {//1.获取当前的时间t1 := time.Now()fmt.Printf("%T\n", t1) //time.Timefmt.Println(t1) //2023-06-10 11:16:15.340273 +0800 CST m=+0.003384001//2.获取指定的时间t2 := time.Date(2008, 7, 15, 16, 30, 28, 0, time.Local)fmt.Println(t2) //2008-07-15 16:30:28 +0800 CST//3.time-->string之间的转换/*t1.Format("格式模板")-->string模板的日期必须是固定:06-1-2-3-4-5*/s1 := t1.Format("2006-1-2 15:04:05")fmt.Println(s1) //2023-6-10 11:16:15s2 := t1.Format("2006/01/02")fmt.Println(s2) //2023/06/10//string-->time/*time.Parse("模板",str)-->time,err*/s3 := "1999年10月10日" //stringt3, err := time.Parse("2006年01月02日", s3)if err != nil {fmt.Println("err:", err)}fmt.Println(t3)fmt.Printf("%T\n", t3)fmt.Println(t1.String())//4.根据当前时间,获取指定的内容year, month, day := t1.Date() //年,月,日fmt.Println(year, month, day) //2023 June 10hour, min, sec := t1.Clock()fmt.Println(hour, min, sec) //时,分,秒year2 := t1.Year()fmt.Println("年:", year2)fmt.Println(t1.YearDay())month2 := t1.Month()fmt.Println("月:", month2)fmt.Println("日:", t1.Day())fmt.Println("时:", t1.Hour())fmt.Println("分钟:", t1.Minute())fmt.Println("秒:", t1.Second())fmt.Println("纳秒:", t1.Nanosecond())fmt.Println(t1.Weekday()) //Wednesday//5.时间戳:指定的日期,距离1970年1月1日0点0时0分0秒的时间差值:秒,纳秒t4 := time.Date(1970, 1, 1, 1, 0, 0, 0, time.UTC)timeStamp1 := t4.Unix() //秒的差值fmt.Println(timeStamp1)timeStamp2 := t1.Unix()fmt.Println(timeStamp2)timeStamp3 := t4.UnixNano()fmt.Println(timeStamp3) //3600 000 000 000timeStamp4 := t1.UnixNano()fmt.Println(timeStamp4)//6.时间间隔t5 := t1.Add(time.Minute)fmt.Println(t1)fmt.Println(t5)fmt.Println(t1.Add(24 * time.Hour))t6 := t1.AddDate(1, 0, 0)fmt.Println(t6)d1 := t5.Sub(t1)fmt.Println(d1)//7.睡眠time.Sleep(3 * time.Second) //让当前的程序进入睡眠状态fmt.Println("main。。。over。。。。。")//睡眠[1-10]的随机秒数rand.Seed(time.Now().UnixNano())randNum := rand.Intn(10) + 1 //intfmt.Println(randNum)time.Sleep(time.Duration(randNum) * time.Second)fmt.Println("睡醒了。。")
}
输出结果:
time.Time
2023-06-10 11:24:17.1424817 +0800 CST m=+0.003008901
2008-07-15 16:30:28 +0800 CST
2023-6-10 11:24:17
2023/06/10
1999-10-10 00:00:00 +0000 UTC
time.Time
2023-06-10 11:24:17.1424817 +0800 CST m=+0.003008901
2023 June 10
11 24 17
年: 2023
161
月: June
日: 10
时: 11
分钟: 24
秒: 17
纳秒: 142481700
Saturday
3600
1686367457
3600000000000
1686367457142481700
2023-06-10 11:24:17.1424817 +0800 CST m=+0.003008901
2023-06-10 11:25:17.1424817 +0800 CST m=+60.003008901
2023-06-11 11:24:17.1424817 +0800 CST m=+86400.003008901
2024-06-10 11:24:17.1424817 +0800 CST
1m0s
main。。。over。。。。。
7
睡醒了。。