Go-基础
为什么要学习 Go,Go 的适用场景,解决了什么为题?
-
并行支持,适合高性能分布式服务
-
云计算
GO的特别之处
-
允许返回多个值
-
Go中的数据类型都是放到最后的(参数声明和定义等)
-
接口是非侵入的,只要实现了接口的所有方法就被认作实现了接口,不用显示的声明
-
代码规范强制统一
-
错误处理
-
并发编程
困惑
站在GO的角度多想想GO为什么这样做而不是一直与Java做对比
环境准备
GOROOT
Go的安装目录
将GOROOT/bin
配置到环境变量PATH中,方便我们在全局中使用Go
GOPATH
指定我们的开发工作区(workspace),是存放源代码、测试文件、库静态文件、可执行文件的工作。
当我们使用go get
命令去获取远程库的时候,安装到工作区
构建工程时,用来查找源码的位置
GOBIN
开发程序编译后二进制命令的安装目录
使用go install
命令编译和打包应用程序时,该命令会将编译后二进制程序打包GOBIN目录
HelloWord
package main
import "fmt"
func main() {
fmt.Printf("Hello World")
}
- 程序从main包的main函数开始运行
- 开头必须指定所属的package,一般是文件夹的名字,但也可以是其他的名字
- main函数没有参数和返回值,参数可以通过命令行传入,命令行传入的参数在os.Args变量中保存
导入包
import "fmt"
导入多个包时可以只写一个import
import (
"fmt"
"math"
)
不能导入用不到的包,否则编译报错
注释
支持两种注释:
行注释://
块注释:/* */
变量
变量: 一块存储空间的名字,我们可以通过变量名来使用这块内存。
变量声明
var name type
与 Java 不同,变量名在前,变量类型在后
声明多个变量
- 类型相同
var (
name string
age int
)
避免写多个 var
- 类型不同
var 变量[,变量1] 类型
初始化
声明并初始化
var age int = 18
// 类型可以推导
var age =18
// var 可以用:=替代
age :=18
// 多个变量进行初始化
age, name := 18, "Tom"
变量赋值
Go提供了多重赋值功能
i,j := 1,2
i, j = j, i
匿名变量
在调用函数时为了获取一个值,却因为该函数返回多个值而不得不定义一堆没用的变量时,其他变量不用声明,用_
表示。
常量
常量的定义
通过const关键字定义
const name type = value
常量的类型是非必须的,可以推断出来。
常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值.
常量赋值是编译期的行为,所以不能赋运行期才知道的结果。
与 import 和变量声明类似,通过()可以一次声明多个常量
const name1, name2 = value1, value2
const(
name1 = value1
name2
)
在常量声明中,如果一个常量没有赋值,则他就跟上一行的赋值相同
字面常量
就是硬编码的常量。Go的字面常量是无类型的,-12可以给int,uint,int32,float等
预定义常量
Go语言预定义了这些常量:true、false和iota
iota
-
iota是一个特殊的常量,可以当成是一个可以被编译器修改的常量
-
iota第一次出现时的值为0, 在const中iota出现一次加一
-
每出现 const 时 iota 清零
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 = iota // c1 == 1
c2 = iota // c2 == 2
)
const (
a = 1 << iota // a == 1 (iota在每个const开头被重设为0)
b = 1 << iota // b == 2
c = 1 << iota // c == 4
)
const (
u = iota * 42 // u == 0
v float64 = iota * 42 // v == 42.0
w = iota * 42 // w == 84
)
const x = iota // x == 0 (因为iota又被重设为0了)
const y = iota // y == 0 (同上)
// 如果两个const的赋值语句的表达式是一样的,那么可以省略后一个赋值表达式。因此,上面的前两个const语句可简写为:
const ( // iota被重设为0
c0 = iota // c0 == 0
c1 // c1 == 1
c2 // c2 == 2
)
const (
a = 1 <<iota // a == 1 (iota在每个const开头被重设为0)
b // b == 2
c // c == 4
)
常量的使用
包名.常量名
枚举
Go不支持 enum,通常用 const 定义一系列相关的常量,这种定义法在Go语言中通常用于定义枚举值。
const (
Unknown = 0
Female = 1
Male = 2
)
数据类型
内置的基础类型
内置类型:不用 import 直接使用的类型
- 布尔类型:bool,不接收其他类型的赋值,1不是 true。
- 整型:int8、byte、int16、int、uint、uintptr等,int,uint 的范围与平台相关。int和int32在Go语言里被认为是两种不同的类型,编译器也不会帮你自动做类型转换,需要强制类型转换(需要注意长度截断和值溢出的问题)。
- 浮点类型:float32、float64,小数默认推导成 float64。
- 复数类型:complex64、complex128。
- 字符串:string。 可以使用反引号来包含双引号
- 字符类型:byte(uint8)字符串的单个字节值,rune(int32)。出于简化语言的考虑,Go语言的多数API都假设字符串为UTF-8编码。尽管Unicode字符在标准库中有支持,但实际上较少使用。
- 错误类型:error。
浮点型
import "math"
// p为用户自定义的比较精度,比如0.00001
func IsEqual(f1, f2, p float64) bool {
return math.Fdim(f1, f2) < p
}
字符串
字符串初始化后不能被修改(即不能通过字符数组的下标来修改)
len
println(len("你好"))
运行结果为 6。实际 len 返回的不是字符数,而是字符串所占的字节数。UTF-8 编码中,一个中文占三个字节
遍历字符串
遍历字符串遍历的是字符串的字节值
- 通过 fori
s := "Hell0 世界"
for i := 0; i < len(s); i++ {
println(i, s[i])
}
- 通过 for range,以 Unicode 字符方式遍历
for index, value := range s {
println(index, value)
}
区别
- fori 打印了 11 个字节值
- for range 打印了 8 个 Unicode 值
复合类型
- 指针(pointer)
- 数组(array)
- 切片(slice)
- 字典(map):Golang 的 Map 类似 Java 的 HashMap,是无序的
- 通道(chan)
- 结构体(struct)
- 接口(interface)
数组
声明
[32]byte // 长度为32的数组,每个元素为一个字节
[2*N] struct { x, y int32 } // 复杂类型数组
[1000]*float64 // 指针数组
[3][5]int // 二维数组
[2][2][2]float64 // 等同于[2]([2]([2]float64))
初始化
// 初始化列表进行初始化
arr := [3]int{1, 2}
// 省略初始化长度
arr := [...]int{1, 2}
// 制定索引值进行初始化
arr := [...]int{1: 1, 2: 1}
遍历数组:
- 获得len,for循环通过下标访问
- range,range会返回两个值:下表和元素值
for i, v := range array {
fmt.Println("Array element[", i, "]=", v)
}
在Go语言中数组是一个值类型, 值类型变量在赋值和作为参数传递是都是复制操作,产生一个副本。
数组切片
- 数组长度不能改变,切片可以动态扩容
- 数组是值类型,切片不会重复复制
数组切片包含三个变量
- 指向原始数组的指针
- 切片中元素的个数
- 切片已分配的存储空间
创建切片
- 基于数组
// 先定义一个数组
var myArray [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 基于数组创建一个数组切片
var mySlice []int = myArray[:5]
- 直接创建
// 创建一个元素个数为 5 的切片
mySlice1 := make([]int, 5)
// 提前指定存储空间大小,减少底层重新分配内存的次数
mySlice2 := make([]int, 5, 10)
mySlice3 := []int{1, 2, 3, 4, 5}
- 基于切片创建新的切片
oldSlice := []int{1, 2, 3, 4, 5}
newSlice := oldSlice[:3] // 指定范围创建新的切片
指定范围可以超过 len,不能超过 cap
cap与 len
cap 获得切片存储空间个数,len 获得当前存储的元素个数
遍历切片
fori
或者 for range
动态增减
append
// 在 mySlice 后添加三个元素然后生成一个新切片
mySlice = append(mySlice, 1, 2, 3)
mySlice2 := []int{8, 9, 10}
// 给mySlice后面添加另一个数组切片生成一个新切片
mySlice = append(mySlice, mySlice2...)
...表述将元素取出来传进去
delete
没有删除的方法,需要借助append生成一个新的切片
// 要删除下标为i的元素
s = append(s[:i],s[i+1:]...)
copy()
为了不共享同一块内存
直接赋值复制的是地址值,用的是同一个切片
如果加入的两个数组切片不一样大,就会按其中较小的那个数组切片的元素个数进行复制。
copy(target, source)
map
声明
var 变量名 map [key类型] value类型
与Java不同key的类型写到[]中,value写到[]外。因为使用过[key]来获得value的
创建,通过make
make(map [KeyType] ValueType)
// 创建并指定出事存储能力
make(map [KeyType] ValueType, 初始容量)
创建并初始化
map[string]int{
"Tom":18,
"Jerry":19,
}
注意最后一个键值对后也需要逗号
赋值
myMap["key"] = value
删除
delete(数组名, key)
检查key是否存在
value, ok := myMap["1234"]
if ok { // 找到了
// 处理找到的value
}
遍历
通过range遍历切片或map,每次循环会返回下表和值
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
func main() {
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
可以使用_
忽略下标或者值,如果只给一个变量,那这个变量表示下标
类型转换
强制类型转换
a :=1
b :=float64(a)
字符串与数字的转换
流程控制
条件判断
条件语句
if a < 5 {
return 0
} else {
return 1
}
- if后不用括号
- 无论后面有几行,{}是必须的
if语句可以包含一个前置语句,使用范围是该if语句内(包含else)
if a:=0; a < 5 {
return 0
} else {
return 1
}
选择语句
func example(x int) int {
switch x {
case 0:
fmt.Printf("0")
case 1:
fmt.Printf("1")
case 2:
fallthrough
case 3:
fmt.Printf("3")
case 4, 5, 6:
fmt.Printf("4, 5, 6")
default:
fmt.Printf("Default")
}
}
- switch自动加了break
- 只有明确添加fallthrough关键字,才会紧接着执行下一个 case
switch {
case 0 <= Num && Num <= 3:
fmt.Printf("0-3")
case 4 <= Num && Num <= 6:
fmt.Printf("4-6")
case 7 <= Num && Num <= 9:
fmt.Printf("7-9")
}
- switch后的语句不是必须的,在此种情况下,整个switch结构与多个if...else...的逻辑作用等同,多个if-else时使用switch更简洁
循环
只支持for循环
for init; condition; modify {
}
前置语句和后置语句是非必需的
for ; condition; {
}
也可以不指定条件无限循环
for {
println("hello")
}
跳转
func myfunc() {
i := 0
HERE:
fmt.Println(i)
i++
if i < 10 {
goto HERE
}
}
跳转到某个标签
range
for搭配range,可以用来遍历字符串,数组,切片,map和信道
- 对于字符串,数组和切片:range有两个返回值,第一个返回值是下表,第二个是元素的值,只有一个返回值时表示下表
func main() {
ints := []int{1, 2, 3}
for index, value := range ints {
fmt.Printf("%d, %d\n", index, value)
}
}
- 对于map:第一个是key,第二个是value
m := map[int]string{1: "Tom", 2: "Tony"}
for key, value := range m {
fmt.Printf("%d, %s\n", key, value)
}
- 对于channel,就是信道中的值
go func() {
for i := 0; i < 10; i++ {
c <- i
}
close(c)
}()
for i := range c {
fmt.Printf("%d\n", i)
}
函数
定义
由func关键字、函数名、参数列表、返回值列表、函数体和返回语句构成
func Add(a, b int) (ret int, err error) {
// TODO do something
return
}
- 可以有多个返回值,包含错误,成功时error返回nil。这样我们不用在去专门定义一个结构体
- 就像函数的输入参数一样。 返回值被命名之后,它们的值在函数开始的时候被自动初始化为空。在函数中执行不带任何参数的return语句时,会返回对应的返回值变量的值。
- 参数列表中类型相同的可以只写最后一个参数的一个类型,返回值相同
- 返回值只有一个值时,可以省略括号
- 小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用
- 命名返回值:返回值可以被函数当成是一个变量,对返回值进行赋值,最后只写一个return,或者最后return 返回值
不定参数: 如...type
格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。
高阶函数
函数可以用来作为参数或返回值
匿名函数
匿名函数:不带函数名的函数
花括号中直接接参数列表或者用变量加参数列表进行调用。
f := func(x, y int) int {
return x + y
}
闭包
闭包:内部函数引用外部函数的变量。
闭包的价值:
别的函数都是执行完里面的变量就结束了,闭包中的变量会被内部变量保持
闭包的使用场景