Go基本语法 程序入口
必须是main包 package main
必须是main方法 func main()
文件名不一定是main.go
文件夹名不一定是main
获取返回值 Go语言的main函数不支持任何返回值 因此return
是不可以使用的 通过使用os.Exit()
来返回状态
获取命令行参数 Go语言的main函数不支持传入参数 在程序中直接通过os.Args
获取命令行参数
1 2 3 4 5 func main () { if len (as.Args) > 1 { fmt.Println("It's" , os.Args[1 ]) } }
声明变量 1 2 3 4 5 6 7 8 var name string = "Ruojhen" var id int = 123 var name = "shaoyuanhang@outlook.com" var decimal = 0.06 var decimal float32 = 0.03
1 2 3 4 5 6 7 8 9 var ( id int =132456 name = "syh" gender = "男" ) fmt.Print(id,name,gender)
1 2 3 4 5 6 7 8 9 name:="syh" var name = "syh" var name string = "syh" id,name:= 123 ,"syh" var a int =100 var b int =200 b,a=a,b fmt.Print(a," " ,b)
1 2 3 4 5 6 7 8 9 10 11 12 13 var age int =100 var address =&agefmt.Print(age," " ,address) ptr:=new (int ) fmt.Println("ptr address" ,ptr) fmt.Println("ptr value" ,*ptr)
环境变量 三个环境变量 GOROOT GOPATH GOBIN GOROOT Go语言的安装路径 GOPATH 若干工作区目录的路径 是自己定义的工作空间 GOBIN Go程序生成的可执行文件的路径
其中 GOPATH的概念最多 GOPATH的意义也是被询问的最多的
回答是: 可以把GOPATH简单理解成Go语言的工作目录 GOPATH的值是一个目录的路径 每个目录都代表Go语言的一个工作区 利用工作区下的文件夹src去存放源码文件(.go) pkg存放平台相关目录(linux_amd64)内存放归档文件( .a) bin存可执行文件(**.exe)
1 2 归档文件就是在linux下是.a的文件 是archive文件 是程序编译后生成的静态库文件 与Java的jar包不同 jar属于动态链接库
Go命令 1 2 3 go build go run go install
数据类型 fmt输出格式 1 2 3 4 5 6 7 8 9 10 %b 表示为二进制 %c 该值对应的unicode码值 %d 表示为十进制 %o 表示为八进制 %q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示 %x 表示为十六进制,使用a-f %X 表示为十六进制,使用A-F %U 表示为Unicode格式:U+1234,等价于"U+%04X" %E 用科学计数法表示 %f 用浮点数表示
整型 int
int8
int16
int32
int64
有符号整型uint
uint8
uint16
uint32
uint64
无符号整型
不同进制整型表示 十进制
二进制 0b前缀
八进制 0o前缀
十六进制
输出格式对比
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport ( "fmt" ) func main () { var num01 int = 0 b1100 var num02 int = 0 o14 var num03 int = 0xC fmt.Printf("2进制数 %b 表示的是: %d \n" , num01, num01) fmt.Printf("8进制数 %o 表示的是: %d \n" , num02, num02) fmt.Printf("16进制数 %X 表示的是: %d \n" , num03, num03) }
浮点型 浮点型表示有两种表示方法 以0.037为例 可以直接用0.037
亦或使用3.7E-2
表示0.037 同理3.7E+1 = 37
有时候浮点型类型值可以被简化 37.0
可以简化为37
0.037
可以简化为.037
浮点数的整数部分和小数部分只能由十进制表示
Go提供了两种精度的浮点数float32
float64
float32
单精度 存储占用4个字节 32位float64
双精度 存储占用8个字节 64位
byte/rune byte byte
占用一个字节 8个比特位 2^8=256 所以byte
类型的范围是 0-255 和uint8
本质上没有区别 byte
表示的是ACSII表中的一个字符
1 2 3 4 5 6 7 8 9 10 11 12 13 import "fmt" func main () { var a byte = 65 var b uint8 = 66 fmt.Printf("a 的值: %c \nb 的值: %c" , a, b) }
输出结果为
在 ASCII 表中,由于字母 A 的ASCII 的编号为 65 字母 B 的ASCII 编号为 66 所以上面的代码也可以写成这样
1 2 3 4 5 6 7 import "fmt" func main () { var a byte = 'A' var b uint8 = 'B' fmt.Printf("a 的值: %c \nb 的值: %c" , a, b) }
rune rune
占用四个字节 32个比特位 所以和uint32
本质上没有区别 rune
表示的是一个Unicode字符
1 2 3 4 5 6 7 8 9 10 import ( "fmt" "unsafe" ) func main () { var a byte = 'A' var b rune = 'B' fmt.Printf("a 占用 %d 个字节数\nb 占用 %d 个字节数" , unsafe.Sizeof(a), unsafe.Sizeof(b)) }
输出结果为
表示中文字符要用rune类型
既然 byte
和 uint8
没有区别 rune
和 uint32
没有区别 那为何还要多设置这两个类型呢
因为uint8
uint32
直观上让人理解为这是一个整型变量 是一个数值 但实际上它也可以表示一个字符 为了消除这个直观错觉 应运而生了byte
rune
字符串 1 var mystr string = "hello"
byte
和rune
都是字符类型 多个字符放在一起就组成了字符串 即string
hello在ASCII表中对应的数字分别是 104 101 108 108 111
1 2 3 4 5 6 7 8 9 10 import ( "fmt" ) func main () { var mystr01 string = "hello" var mystr02 [5 ]byte = [5 ]byte {104 , 101 , 108 , 108 , 111 } fmt.Printf("mystr01: %s\n" , mystr01) fmt.Printf("mystr02: %s" , mystr02) }
输出结果为
1 2 mystr01: hello mystr02: hello
mystr01 mystr02输出内容一样 说明string
的本质是byte
数组
Go语言的string
使用utf-8进行编码 英文字母占1个字节 中文汉字占3个字节
字符串不仅仅可以使用双引号来表示 还可以使用反引号 反引号通常应用于存在转移字符\
的情况下
被反引号包裹的字符串会被忽略掉其中的转义字符
1 2 var mystr01 string = "\\r\\n" var mystr02 string = `\r\n`
这两个字符串的打印结果都是\r\n
1 2 3 4 5 6 7 8 9 import ( "fmt" ) func main () { var mystr01 string = `\r\n` fmt.Printf(`\r\n` ) fmt.Printf("的解释型字符串是: %q" , mystr01) }
输出结果是
同时反引号可以不写换行符\n来表示一个多行的字符串 因为换行符会被忽略掉
1 2 3 4 5 6 7 8 9 10 import ( "fmt" ) func main () { var mystr01 string = `Hello Shaoyuanhangyes Ruojhen` fmt.Println(mystr01) }
输出结果
1 2 Hello Shaoyuanhangyes Ruojhen
数组 数组声明 1 2 3 4 var arr [3 ]int arr[0 ] = 1 arr[1 ] = 2 arr[2 ] = 3
1 2 3 4 5 var arr [3 ] = [3 ]int {1 ,2 ,3 }arr := [3 ]int {1 ,2 ,3 },
3表示数组元素个数 可以使用...
来让系统自己根据实际情况来分配空间
值得注意的是 [3]int
[4]int
虽然都是数组 但类型却不同 使用fmt的%T
来查得
1 2 3 4 5 6 7 8 9 10 import ( "fmt" ) func main () { arr01 := [...]int {1 , 2 , 3 } arr02 := [...]int {1 , 2 , 3 , 4 } fmt.Printf("%d 的类型是: %T\n" , arr01, arr01) fmt.Printf("%d 的类型是: %T" , arr02, arr02) }
输出结果为
1 2 [1 2 3] 的类型是: [3]int [1 2 3 4] 的类型是: [4]int
type 使用关键字type
来起别名
1 2 3 4 5 6 7 8 9 10 import ( "fmt" ) func main () { type arr3 [3 ]int myarr := arr3{1 ,2 ,3 } fmt.Printf("%d 的类型是: %T" , myarr, myarr) }
输出结果为
只初始化部分位置
打印arr的结果为 [0 0 3 0]
切片 切片是对数组的一个连续片段的引用 所以切片是一个引用类型 这个片段可以是整个数组 也可以是数组的子集 但这个片段是一个左闭右开的区间
即终止索引对应的项不在这个片段中
1 2 3 4 5 6 7 8 import ( "fmt" ) func main () { myarr := [...]int {1 , 2 , 3 } fmt.Printf("%d 的类型是: %T" , myarr[0 :2 ], myarr[0 :2 ]) }
输出结果为
切片是引用类型 所以未给予初值的切片的默认值是nil
切片构造方式 对数组进行片段截取 1 2 3 4 5 myarr := [5 ]int {1 ,2 ,3 ,4 ,5 } mysli1 := myarr[1 :3 ] mysli2 := myarr[1 :3 :4 ]
mysli1 mysli2打印结果一模一样 其中mysli2中的4是做什么的呢
在切片时 若不指定第三个数 那么切片的终止索引会一直到原数组的最后一个数 若指定了第三个数 那么切片的终止索引就到这第三个数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" func main () { myarr := [5 ]int {1 ,2 ,3 ,4 ,5 } fmt.Printf("myarr 的长度为:%d,容量为:%d\n" , len (myarr), cap (myarr)) mysli1 := myarr[1 :3 ] fmt.Printf("mysli1 的长度为:%d,容量为:%d\n" , len (mysli1), cap (mysli1)) fmt.Println(mysli1) mysli2 := myarr[1 :3 :4 ] fmt.Printf("mysli2 的长度为:%d,容量为:%d\n" , len (mysli2), cap (mysli2)) fmt.Println(mysli2) }
输出结果为
1 2 3 4 5 myarr 的长度为:5,容量为:5 mysli1 的长度为:2,容量为:4 [2 3] mysli2 的长度为:2,容量为:3 [2 3]
所以切片的第三个数只影响切片的容量 不影响切片的长度
从头声明赋值 1 2 3 4 5 var strList[]string var numList[]int var numListEmpty = []int {}
可以看到 声明切片类型和声明数组类型基本上是一模一样 除了不需要说明长度
make函数构造切片 make([]Type,size,cap)
Type类型 size长度 cap容量
1 2 3 4 5 6 7 8 9 10 11 import ( "fmt" ) func main () { a := make ([]int , 2 ) b := make ([]int , 2 , 10 ) fmt.Println(a, b) fmt.Println(len (a), len (b)) fmt.Println(cap (a), cap (b)) }
输出结果为
因此使用make函数构造切片的时候不指定容量的时候 容量等于长度值
只初始化部分位置 和数组一样
1 2 3 4 5 6 7 8 9 import ( "fmt" ) func main () { a := []int {4 :2 } fmt.Println(a) fmt.Println(len (a), cap (a)) }
输出结果为
数组与切片 数组与切片都是可容纳若干类型相同元素的容器 不同点在于 数组容器大小固定 而切片本身是引用类型 可以append进行元素添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import ( "fmt" ) func main () { myarr := []int {1 } myarr = append (myarr, 2 ) myarr = append (myarr, 3 , 4 ) myarr = append (myarr, []int {7 , 8 }...) myarr = append ([]int {0 }, myarr...) myarr = append (myarr[:5 ], append ([]int {5 ,6 }, myarr[5 :]...)...) fmt.Println(myarr) }
输出结果为
可以如此理解数组与切片的区别 切片是引用类型 数组是值类型 数组长度固定 切片是动态的数组 切片比数组多一个属性 容量cap 切片的底层实现是数组 切片不能进行等值判断 只能和nil判断
字典 字典 Map 若干个key:value
组建在一起的无序键值对 要求每一个key都是唯一的 可以使用==
!=
来进行判断 key不能是切片 不能是字典 不能是函数 map是一个集合 可以迭代它 不过map是无序的 无法决定他的返回顺序 因为map是使用hash表实现的 声明map时必须指定好key和value的类型
声明初始化字典 1 2 3 4 5 6 7 8 var scores map [string ]int = map [string ]int {"English" :80 ,"Chinese" :90 }scores := map [string ]int {"English" :80 ,"Chinese" :90 } scores := make (map [string ]int ) scores["English" ] = 80 scores["Chinese" ] = 90
需要注意的是 只声明不初始化的map类型的初值为nil 无法直接进行赋值 所以需要使用make函数先对其初始化 然后才能直接赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import "fmt" func main () { var scores map [string ]int if scores == nil { scores = make (map [string ]int ) } scores["chinese" ] = 90 fmt.Println(scores) }
打印结果为 map[chinese:90]
对字典的操作 1 2 3 4 5 6 7 scores := make (map [string ]int ) scores["math" ] = 150 fmt.Print(scores["math" ]) delete (scores,"math" )
判断key是否存在 因为有可能key对应的value恰好为零值 所以想要判断key是否存在不能够使用判断value零值的办法
字典的下标读取会返回两个值 可以使用第二个返回值来表示对应的key是否存在 若存在 ok则为true 若不存在 ok为false
1 2 3 4 5 6 7 8 9 10 import "fmt" func main () { scores := map [string ]int {"english" : 80 , "chinese" : 85 } if math, ok := scores["math" ]; ok { fmt.Printf("math 的值是: %d" , math) } else { fmt.Println("math 不存在" ) } }
对字典进行循环 1 2 3 4 5 6 7 8 9 import "fmt" func main () { scores := map [string ]int {"english" : 80 , "chinese" : 85 } for subject, score := range scores { fmt.Printf("key: %s, value: %d\n" , subject, score) } }
1 2 3 4 5 6 7 8 9 import "fmt" func main () { scores := map [string ]int {"english" : 80 , "chinese" : 85 } for subject := range scores { fmt.Printf("key: %s\n" , subject) } }
1 2 3 4 5 6 7 8 9 import "fmt" func main () { scores := map [string ]int {"english" : 80 , "chinese" : 85 } for _, score := range scores { fmt.Printf("value: %d\n" , score) } }
布尔类型 与其他语言不同的一点 go的true不等于1 false不等于0 即 bool
和int
不能直接转换 需要自己实现转换函数
1 2 3 4 5 6 7 func bool2int (b bool ) int { if b { return 1 } return 0 }
1 2 3 4 func int2bool (i int ) bool { return i != 0 }
指针 创建指针 1 2 3 4 5 6 7 8 9 10 11 12 aint :=1 ptr1 := &aint astr := new (string ) *astr ="Ruojhen" aint := 1 var bint *int bint = &aint
以防混淆
1 2 3 4 5 6 7 8 9 10 import "fmt" func main () { aint := 1 ptr := &aint fmt.Println("普通变量存储的是:" , aint) fmt.Println("普通变量存储的是:" , *ptr) fmt.Println("指针变量存储的是:" , &aint) fmt.Println("指针变量存储的是:" , ptr) }
打印结果为
1 2 3 4 普通变量存储的是: 1 普通变量存储的是: 1 指针变量存储的是: 0xc0000100a0 指针变量存储的是: 0xc0000100a0
打印指针指向的内存地址
1 2 3 4 5 fmt.Printf("%p" , ptr) fmt.Println(ptr)
指针的类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import "fmt" func main () { astr := "hello" aint := 1 abool := false arune := 'a' afloat := 1.2 fmt.Printf("astr 指针类型是:%T\n" , &astr) fmt.Printf("aint 指针类型是:%T\n" , &aint) fmt.Printf("abool 指针类型是:%T\n" , &abool) fmt.Printf("arune 指针类型是:%T\n" , &arune) fmt.Printf("afloat 指针类型是:%T\n" , &afloat) }
打印结果为
1 2 3 4 5 astr 指针类型是:*string aint 指针类型是:*int abool 指针类型是:*bool arune 指针类型是:*int32 afloat 指针类型是:*float64
可以发现用 *
+所指向变量值的数据类型就是对应的指针类型
指针的初始值 指针声明后不初始化赋值的话 指针的值就会是nil
切片与指针 指针和切片一样都是引用类型
若想通过一个函数改变一个数组内的值 有两种方式
1.将数组的切片作为参数传给函数 2.将数组的指针作为参数传给函数
按照go的语言习惯 使用切片将会更加简洁
1 2 3 4 5 6 7 8 9 10 func modify (sls []int ) { sls[0 ] = 90 } func main () { a := [3 ]int {89 , 90 , 91 } modify(a[:]) fmt.Println(a) }
1 2 3 4 5 6 7 8 9 10 func modify (arr *[3]int ) { (*arr)[0 ] = 90 } func main () { a := [3 ]int {89 , 90 , 91 } modify(&a) fmt.Println(a) }
go指针限制 相较于cpp的指针 go指针有着许多限制 目的是避免指针操作的危险
限制一 指针不能参与数学运算
限制二 不同类型的指针不允许相互转换
限制三 不同类型的指针不能比较和相互赋值
流程控制 For循环 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 a := 1 for a<=5 { fmt.Println(a) a++ } for i := 1 ; i <= 5 ;i++ { fmt.Println(i) } for {} for ;; {}
for range 遍历一个可迭代的数据结构用for range语句实现 range后可接数组 切片 字符串等
按照以往的编程习惯 随便写一个循环
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" ) func main () { vector := [...]string {"C++" ,"Go" ,"Git" ,"Redis" } for i := range vector { fmt.Println("I will learn " ,i) } }
输出结果为
1 2 3 4 I will learn 0 I will learn 1 I will learn 2 I will learn 3
发现与我们期望的不符 做一些小更改
1 2 3 4 5 6 7 8 9 10 11 12 13 package mainimport ( "fmt" ) func main () { vector := [...]string {"C++" ,"Go" ,"Git" ,"Redis" } for _, i := range vector { fmt.Println("I will learn " ,i) } }
输出结果为
1 2 3 4 I will learn C++ I will learn Go I will learn Git I will learn Redis
这是因为range返回两个值 第一个是索引 第二个是数据
defer 延迟语句 defer
后接函数名 就能实现将这个函数的调用延迟到当前函数执行完再调用
理解defer的使用效果
1 2 3 4 5 6 7 func syhprint () { fmt.Println("B" ) } func main () { defer syhprint() fmt.Println("A" ) }
输出结果为
代码也可以简写为
1 2 3 4 func main () { defer fmt.Println("B" ) fmt.Println("A" ) }
defer的快照作用 使用defer只是延时调用了函数 此时已经传递给函数的变量 就像一个快照一样被保存了下来 不会受到后续程序执行的影响
1 2 3 4 5 6 func main () { language := "C++" defer fmt.Println(language) language = "Go" fmt.Println(language) }
输出结果为
由此可见 在变量被修改前延时调用打印函数 即使变量值被修改了 最后延迟调用的函数的变量值依旧是修改前的值 就像是defer给这个函数的变量值做了一个快照
多个defer调用的顺序 1 2 3 4 5 6 7 8 func main () { language := "C++" defer fmt.Println(language) language = "Go" defer fmt.Println(language) language = "Rust" fmt.Println(language) }
输出结果为
由此可见 多个defer一起调用的时候 输出的结果是反序的 类似栈的后进先出
defer/return defer和return同时存在的话 先调用谁呢 看下面的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import ( "fmt" ) var name string = "C++" func rename () string { defer func () { name = "Go" }() fmt.Println("rename函数内的name为" ,name) return name } func main () { myname := rename() fmt.Println("main函数里的name为" ,name) fmt.Println("main函数里的myname为" ,myname) }
输出结果为
1 2 3 rename函数内的name为 C++ main函数里的name为 Go main函数里的myname为 C++
对代码进行剖析 理解为何是这个输出结果
1 2 3 4 5 6 7 8 9 10 11 在main函数里初始化myname的时候 调用了rename函数 rename函数内输出了name值为全局变量的"C++" 同时rename函数内延迟修改了全局变量name的值为"Go" main函数输出name值 为修改后的"Go" main函数输出myname值 为rename函数的返回值"C++" 因此我们发现在rename函数调用完毕后 才会延迟修改全局变量name的值 所以return 回的值为修改前的"C++" 因此return 的调用在defer之前 毕竟要完成函数后才会继续延迟调用
为什么要有defer 能够缩减代码量 使代码不那么臃肿 下面是一段经典的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 func f () { r := getResource() ...... if ... { r.release() return } ...... if ... { r.release() return } ...... if ... { r.release() return } ...... r.release() return } func f () { r := getResource() defer r.release() ...... if ... { ... return } ...... if ... { ... return } ...... if ... { ... return } ...... return }
异常 Go语言中 异常的抛出和捕获依赖两个内置函数
panic 抛出异常 使程序崩溃
recover 捕获异常 恢复程序 recover调用后 抛出的panic将会在此处终结 不会再向外抛出异常 并且recover必须在defer修饰下才能发挥作用
panic 手动抛出异常
1 2 3 func main () { panic ("程序出错了" ) }
控制台输出效果为
1 2 3 4 5 6 panic: 程序出错了 goroutine 1 [running]: main.main() d:/Code/My Code/Golang/Hexomd/src/github.com/Ruojhen/hexomd01/test.go:8 +0x40 exit status 2
recover 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 func data (x int ) { defer func () { if err := recover (); err != nil { fmt.Println(err) } }() var arr [10 ]int arr[x] = 25 } func main () { data(20 ) fmt.Println("It is fine" ) } `` ` 控制台输出结果为 ` `` bashruntime error : index out of range [20 ] with length 10 It is fine