Go语言学习整理
本文基于菜鸟教程,对于自己不明白的点加了点个人注解,对于已明确的点做了删除,可能结构不太清晰,看官们可移步Go语言教程
1 Go语言结构
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。
起始的{不能单独成行(使用IDE为JetBrain的GoLand,Goland我使用的破解方法都不太好使,也可以直接在线编译运行,网站自行百度)
package main
import "fmt"
func main()
{ // 错误,{ 不能在单独的行上,应该写成func main(){
fmt.Println("Hello, World!")
}
Go的编译与运行
编译生成二进制文件后再执行:
$ go build hello.go
$ ls
hello hello.go
$ ./hello
Hello, World!
直接go run运行:
$ go run hello.go
Hello, World!
在这里的go run与go build的区别,go build会生成对应的可执行二进制文件,go run不会生成对应的二进制文件,会直接运行。
2 Go语言基础语法与数据类型
2.1 基础语法
在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。以下为两个语句:
fmt.Println("Hello, World!")
fmt.Println("菜鸟教程:runoob.com")
标识符必须以字母或者下划线开头,不能以数字开头。
2.2 数据类型
Go 编程语言中,数据类型用于声明函数和变量。数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。Go 语言按类别有以下几种数据类型:
1.bool型 var b bool = true
2.数字类型:int float32 float64 支持整型和浮点型 也支持复数,其中位运算采用补码。
3.字符串
4.派生类型
指针、数组、struct、Channel、函数、切片、接口类型、Map(与Python中的dict类似)
其余类型;uint8 uint16 uint32 uint64 int8 int16 int32 int64 float32 float64
complex64:32位实数与虚数 complex128:64位实数与虚数
byte:类似unit8 rune:类似int32
uintptr:无符号整型 用于存放一个指针
3 Go变量常量运算符
3.1 Go变量
Go与c或者Java相比较而言,最大的特点定义顺序基本是反的,定义变量时会将数据类型放在后面。
比如c或者Java中定义一个变量是int a,定义多个变量是int a, b, c;定义一个指向整型的指针是int* p。
而在Go中这些变量的定义基本上都反过来了,a int;a,b int;定义指针,var var_name *var-type, ip *int; fp *float。
数组定义:C++: int a[10];java: int[] arr = new int[10]; Go: a[10] int.
声明变量:var identifier type
1.指定变量类型,声明后若不赋值,则使用默认值
var v_name v_type
v_name = value
2.根据值自行判断变量类型
Var v_name = value
3.省略var(这种方式在Go中出现次数最多),类似于octave的赋值方法 注意 :=左侧(冒号后接等号)的变量不应该是已经声明过的,否则会导致编译错误。
v_name := value// 例如
var a int = 10
var b = 10
c := 10
3.1.1 多变量声明
package main
var x, y int
var ( // 这种因式分解关键字的写法一般用于声明全局变量
a int
b bool
)
var c, d int = 1, 2
var e, f = 123, "hello"
//这种不带声明格式的只能在函数体中出现
//g, h := 123, "hello"
func main(){
g, h := 123, "hello"
println(x, y, a, b, c, d, e, f, g, h)
}
多变量同时赋值
a, b, c := 5, 7, "abc"
空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。_ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。并行赋值也被用于当一个函数返回多个返回值时,比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到:val, err = Func1(var1)。
3.2 Go常量
const identifier [type] = value
可以省略类型标识符[type],因为编译器可以根据变量的值来推断其类型。
显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"
iota:特殊常量,一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
iota 可以被用作枚举值:
package main
import ("fmt")
func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
}
4 通道
通道(channel)是用来传递数据的一个数据结构,通道在Go中很重要。
goroutine 是 golang 中在语言级别实现的轻量级线程,仅仅利用 go 就能立刻起一个新线程。多线程会引入线程之间的同步问题,在 golang 中可以使用 channel 作为同步的工具。
通过 channel 可以实现两个 goroutine 之间的通信。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
ch <- v // 把 v 发送到通道 ch,向chan传入数据
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
声明通道:
ch := make(chan int)//定义ch是一个接收int类型数据的channel
4.1 默认
默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须又接收端相应的接收数据。
以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // 把 sum 发送到通道 c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // 从通道 c 中接收
fmt.Println(x, y, x+y)
}
4.2 通道缓冲区
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
package main
import "fmt"
func main() {
// 这里我们定义了一个可以存储整数类型的带缓冲通道
// 缓冲区大小为2
ch := make(chan int, 2)
// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2
// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)
}
4.3 Go遍历通道与关闭通道
v, ok := <-ch
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)//这里必须要调用close,不然会报错fatal error: all goroutines are asleep - deadlock!
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
// 会结束,从而在接收第 11 个数据的时候就阻塞了。
for i := range c {
fmt.Print(i," ")
}
}
通道的特点:
通过信道发送和接收数据默认是阻塞的。这是什么意思呢?当数据发送给信道后,程序流程在发送语句处阻塞,直到其他协程从该信道中读取数据。同样地,当从信道读取数据时,程序在读取语句处阻塞,直到其他协程发送数据给该信道(这个特点也非常重要,在后面涉及select和协程是会再次提到)。
5 Go条件与循环
5.1 条件
If else与switch case都与C++/Java基本一致,括号上也许存在细微的差别。
5.1.1 条件中的select语句
select是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。
select随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
语法:
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
以下描述了 select 语句的语法:
每个case都必须是一个通信,即利用chan类型来读写数据,所有channel表达式都会被求值,所有被发送的表达式都会被求值
1.如果任意某个通信可以进行,它就执行;其他被忽略。
2.如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
否则:
如果有default子句,则执行该语句。
如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
package main
import "fmt"
func main() {
var c1, c2, c3 chan int//定义了三个接收整型数据的channel
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
}
这里的c1,c2,c3就和我们之前说的channel的特性串起来了,读要等待写,写要等待读,都被阻塞,所以三个case都没有走,直接走进了default。
执行结果:
疑问?这里如果构建一个新的协程去解锁信道的读写,最后还是会走到default,为什么?如下代码所示:
package main
import "fmt"
func Chann(ch chan int, stopCh chan bool) {
var i int
i = 10
for j := 0; j < 10; j++ {
ch <- i
//time.Sleep(time.Second)
}
stopCh <- true
}
func main() {
var c1, c2, c3 chan int//定义了三个接收整型数据的channel
var i1, i2 int
stopCh := make(chan bool)
go Chann(c1, stopCh)
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
}
结果如下:
为了避免go内部的选择机制一直选default,可以加上select外层加上for,或者去掉default,先去掉default。
可见所有协程阻塞造成死锁:
这个问题困扰了我一段时间,后来明白了,是因为c1 chan没有开辟内存,仅仅是做了一个声明。chan使用没有设置cap,即var c1 = make(chan int, 1)。修改代码进行验证,与猜想原因一致:
5.2 循环语句
5.2.1 无限循环
package main
import "fmt"
func main() {
for true {//不是while
fmt.Printf("这是无限循环。\n");
}
}
5.2.2 for循环
package main
import "fmt"
func main() {
numbers := [6]int{1, 2, 3, 5} /* for 循环 */
b :=0
for a := 0; a < len(numbers); a++ {//C++
fmt.Printf("a 的值为: %d\n", numbers[a])
}
for b < len(numbers) {
fmt.Printf("b 的值为: %d\n", numbers[b])
b++
}
}
//for 循环的 range 格式可以对 slice、map、数组、字符串、Channel等进行迭代循环。格式如下:
//对于数组类型,这里的i即为数组下标,和Python中的enumerate类似。
for i,x:= range numbers {
fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}
5.2.3 Goto
goto的用法与C++一致,在结构化程序设计中不主张使用goto,避免造成程序流程的混乱和理解调试不必要的困难。
语法格式:
goto label
..
...
label: statement
实例:
package main
import (
"fmt"
)
func main() {
var a int = 10
/* 循环 */
LOOP: for a < 20 {
if a == 15 {
/* 跳过迭代 */
a = a + 1
goto 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
Process finished with exit code 0
6 Go语言函数
函数是基本的代码块,用于执行一个任务。
Go 语言最少有个 main() 函数。
你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务。
函数声明告诉了编译器函数的名称,返回类型,和参数。
Go 语言标准库提供了多种可动用的内置的函数。例如,len() 函数可以接受不同类型参数并返回该类型的长度。如果我们传入的是字符串则返回字符串的长度,如果传入的是数组,则返回数组中包含的元素个数。
func function_name( [parameter list] ) [return_types] {
函数体
}
实例
package main
import "fmt"
/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int
if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}
//函数返回多个值
func swap(x, y string) (string, string) {
return y, x
}
func main() {
a, b := swap("Mahesh", "Kumar")
fmt.Println(a, b)
c := max(6,12)
fmt.Println("c: ",c)
}
6.1 函数参数
函数如果使用参数,该变量可称为函数的形参。
形参就像定义在函数体内的局部变量。
调用函数,可以通过两种方式来传递参数:
值传递和引用传递,引用传递是指在调用函数时,在函数体中对形参的修改,影响到了实参。
传引用的方式,和C++的swap方式基本一致。
函数用法:函数作为值,闭包,方法
值:return math.sqrt
6.2 闭包
Go语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。
对于匿名函数,简单记一下自己的认知:Python中也具有匿名函数。举例:map(lambda x: x* x , [1,2,3]),这里的特点是不需要显式定义一个函数,简洁。但是匿名函数还有另一个特点是,匿名函数可以作为一个函数对象把它赋值给一个变量,再利用变量来调用该函数,即使得函数可以像普通变量一样被传递或者使用。
Python示例:
go实例中,我们创建了函数 getSequence() ,返回另外一个函数。该函数的目的是在闭包中递增 i 变量,代码如下:
package main
import "fmt"
func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}
func main() {
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence() /* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Print(nextNumber(), " ")
fmt.Print(nextNumber(), " ")
fmt.Print(nextNumber(), " ")
/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Print(nextNumber1(), " ")
fmt.Print(nextNumber1(), " ")
}
代码最后的执行结果
6.3 方法
Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型/结构体类型的一个值或是一个指针。所有给定类型的方法属于该类型的方法集,个人认为这样即可变相实现C++中的类内方法的类外定义。语法格式如下:
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
实例
package main
import (
"fmt"
)
/* 定义结构体 */
type Circle struct {
radius float64
}
func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}
//该 method 属于 Circle 类型对象中的方法
//特别注意下这里和go中一般函数的定义是有区别的,func function_name( [parameter list] ) [return_types]是一般的函数声明
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
7 Go语言数组
7.1 初始化数组
初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
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}
7.2 多维数组
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
var a = [5][2]int{ {0,0}, {1,2}, {2,4}, {3,6},{4,8}}
8 Go指针
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
指针变量指向一个值的内存地址。*来获取指针所指向的内容,&取变量内存地址
nil与null None nil NULL一样,都指代零值或者空值。
指针数组:var ptr [MAX]*int;
指向指针的指针:**ptr
传指针作为出参,swap函数的示例。
9 Go结构体、slice、range、集合
9.1 结构体
Go的结构体就是别的语言中的类,
type Books struct {
title string
author string
subject string
book_id int
}
如下方法类似构造函数:
结构体指针,即类指针
结构体对于类内函数的实现个人认为可以参看6.3,实现了类内函数的类外定义(C++也可以做到,但是C++需要在类内声明)。
9.2 Slice切片
切片:动态数组
最简单的切片定义:即声明一个未指定大小的数组来定义切片
arr []int
或者使用make函数来创建切片:var slice1 []type = make([]type, len) 也可以简写为slice1 := make([]type, len)
切片初始化: s :=[] int {1,2,3 }
s := arr[startIndex:endIndex] s := arr[startIndex:] s := arr[:endIndex],这几种用法与Python的list的切片用法一致。
append()与copy() api
copy(numbers1,numbers) 拷贝numbers的内容到numbers1
9.3 Range
package main
import "fmt"
func main() {
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
fmt.Println("sum:", sum)
//在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
}
9.4 Map
Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
map的初始化:
1.与Python字典类似
/* map插入key - value对,各个国家对应的首都 */
countryCapitalMap [ "France" ] = "Paris"
countryCapitalMap [ "Italy" ] = "罗马"
2.表达式的方式来进行初始化
countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
delete()函数,参数为map和其对应的mapkey
delete(countryCapitalMap, "France")
10 Go语言接口
Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,并不需要像Java中那样用implements来表示实现接口,任何其他类型只要实现了这些方法就是实现了这个接口。
/* 定义接口 */
type interface_name interface {
method_name1 [return_type]//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 main
import (
"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 Phone
phone = new(NokiaPhone)
phone.call()
phone = new(IPhone)
phone.call()
}
问题:能否说Go面向对象?:多态(接口),继承,封装(方法)。应该可以说Go面向对象,但是Go实际借鉴的编程思想,不止面向对象一种。