go语言语法
go语言
特点
第一个go语言
package main
import "fmt"
func main() {
fmt.Println("Hello world")
}
go语言基础组成
- 包声明
- 引入包
- 函数
- 函数
- 变量
- 语句&表达式
- 注释
各个部分
- 第一行代码 package main 定义了包名。你必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main。package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
- 下一行 import “fmt” 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数 fmt包:format
- 下一行 func main() 是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
- 下一行 /…/ 是注释,在程序执行时将被忽略。单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /* 开头,并以 */ 结尾,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。
- 下一行 fmt.Println(…) 可以将字符串输出到控制台,并在最后自动增加换行字符 \n。
使用 fmt.Print(“hello, world\n”) 可以得到相同的结果。
Print 和 Println 这两个函数也支持使用变量,如:fmt.Println(arr)。如果没有特别指定,它们会以默认的打印格式将变量 arr 输出到控制台。 - 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。
- 即大写字母开头为public , 小写字母开头为protected
执行go
go run hello.go //编译并执行代码
go build hello.go //go build命令来生成二进制文件
生成的二进制文件没有.go后缀,直接 ./文件名 即可运行程序
自动编译脚本
-
#!/usr/bin/env bash CURRENT_DIR=`pwd` OLD_GO_PATH="$GOPATH" #例如: /usr/local/go OLD_GO_BIN="$GOBIN" #例如: /usr/local/go/bin export GOPATH="$CURRENT_DIR" export GOBIN="$CURRENT_DIR/bin" #指定并整理当前的源码路径 gofmt -w src go install test_hello export GOPATH="$OLD_GO_PATH" export GOBIN="$OLD_GO_BIN"
关于包
- 同一个文件夹下的文件只能有一个包名,否则会编译报错
行分隔符
在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。
如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。
fmt.Println("Hello, World!") fmt.Println("菜鸟教程:runoob.com")
Go 语言的空格
Go 语言中变量的声明必须使用空格隔开
var age int;
格式化字符串
fmt.Sprintf
使用fmt.Sprintf格式化字符串并赋值给新串
go语言变量
声明变量的一般形式是使用var关键字
var identifier type
也可以一次声明多个变量
var identifier1,identifier2 type
package main import "fmt" func main(){ var a string = "Runoob" fmt.Println(a) var b,c int = 1,2 fmt.Println(b,c) }
变量声明
第一种,指定变量类型,如果没有初始值,则变量默认为0值
var v_name v_type v_name = v_type
零值就是变量没有做初始化时系统默认设置的值
package main import "fmt" func main(){ var a = "Runoob" fmt.Println(a) var a int fmt.Println(b) //int值默认为0 var c bool fmt.Println(c) //bool值默认是false }
第二种,根据值自行判定变量类型
var v_name = value
第三种,如果变量已经使用var声明过了,再使用 := 声明变量,就产生编译错误
var intVal int //下面不能使用 intVal := 1,因为intVal 已经声明,不需要重新声明
第四种,直接使用 := 来声明即可
intVal := 1 //此时不会产生编译错误,因为有声明新的变量,因为:=是一个声明语句
var intVal int intVal = 1 //和上面:=声明赋值相同
多变量声明
//类型相同的多个变量,而非全局变量
var vname1,vname2,vname3 type
vname1,vname2,vname3 = v1,v2,v3
var vname1,vname2,vname3 = v1,v2,v3
vname1,vname2,vname3 := v1,v2,v3 //出现在:=左侧的变量不应该是已经被声明过的,否则会导致编译错误
我的理解:
// var关键字用来声明 // :=用来在赋值之前进行声明 // 使用 := 不带var关键字声明格式的只能在函数体中出现,不能和全局变量一样放到
声明全局变量的隐式分解的写法
var( vname1 v_type1 vname2 v_type2 )
值类型
- 所有像int,float,bool,string这些基本类型都属于值类型,使用这些类型的变量都直接指向存在内存中的值
- 当使用等号
=
将一个变量的值赋值给另一个变量时,如:j = i
,实际上是在内存中将 i 的值进行了拷贝: - 值类型变量的值都存储在堆中
引用类型
- 一个引用类型的变量存储的是这个变量所在的内存地址,或者内存地址中第一个字所在的位置
- 并不是去直接存值
- 当使用赋值语句r2 = r1时,只有引用地址被复制
- 如果r1的值改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2也会受到影响
注意
- 如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明
- 在函数中单纯地给 a 赋值(将a定义为非全局变量)也是不够的,这个值必须被使用(就是声明定义的变量必须使用)
- 但是全局变量是允许声明但是不使用的,同一个类型的多个变量可以声明在同一行
- 交换两个变量的值:a,b=b,a 两个变量的类型必须相同
- 空白标识符也被用于抛弃值, a,b = _,7 ,将得到结果为a没有值,b的值为7
简短形式
- 使用 := 赋值操作符
- 声明语句写var显得有些多余了,如果不是定义全局变量,则不需要写var
- 变量的首选是用 := ,但是只能被用于函数体内,而不可以用于全局变量的声明与赋值
go语言常量
const identifier [type] = value
可以省略类型说明符,因为编译器可以根据变量的值来推断其类型
const b string = "abc" //显示声明 const b = "abc" //隐式声明
常量可以用作枚举
const ( Unknown = 0 Female = 1 Male = 2 )
常量表达式中,函数必须是内置函数,否则编译不过
const ( a = "abc" b = len(a) c = unsafe.Sizeof(a) )
特殊常量 iota
iota,特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1
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) }
我的理解:第一个iota为0,每更新一行时,iota会自动+1,如果遇到了String类型的数据,String的值不会改变,在const中枚举,如果重新给某个变量进行了赋值,则会自动摒弃iota
左右移
- 左移为 * 2的n次方
- 右移为 / 2的n次方
go语言条件语句
- go没有三目运算符,所以不支持 ?: 形式的条件判断
- go所有的二目运算符都是从左向右执行的
- select语句,如果有多个case都可以执行,select会随机公平地选出一个执行,其它的不会执行
- 如果有default子句,则执行该语句
- 如果没有default子句,select将阻塞,直到某个通信可以运行;go不会重新对channel或值进行求值
循环
for-each循环的第一个元素肯定是下标,第二个元素才是值
for i,s := range strings
使用range关键字
循环中的goto语句
- goto语句可以无条件地转移到过程中指定的行
- goto语句通常与条件语句配合使用,可用来实现条件转移, 构成循环,跳出循环体等功能
- goto loop
go语言数组
等号右边先定义维度,然后定义类型,最后来个大括号
全看:var 数组名 = [size] type {}
var variable_name [SIZE] variable_type
var balance [10] float32 //var 数组名 [size] 类型
初始化,等号,大括号
var balance = [5]float{1,2,3,4,5}
也可以通过字面量在声明数组的同时快速初始化数组
var balance := [5]float{1,2,3,4,5}
在初始化数组时,如果长度不确定,可以使用 … 来代替数组的长度,编译器会根据元素的个数自动推断数组的个数
var balance = [...]float{1,2,3,4} balance := [...]float{1,2,3,4,5}
注意,变量的类型一定在变量名的后面
多维数组
var variable_name [1][2][3] variable_type
var threedim [1][2][3]int
- 初始化二维数组
- 如果最后一个元素和结尾的 } 不位于同一行,则需要最后元素后添加一个逗号
- 如果位于同一行,则可以不带逗号
- 总之,最后一行的 } 不能单独一行
go指针
- 当一个指针被定义后没有分配到任何变量时,它的值为 nil
- nil指针也被称为空指针
- 指针数组存地址,令每一个元素都指向一个值
结构体
type和struct关键字,type语句设定了结构体的名称
type name struct { member definition member member }
变量的声明
variable_name := structure_variable_name {value1,value2,value3}
当定义结构体时,不一定需要使用某个结构体中的所有属性,即可以有几个属性不带
go语言切片
数组长度不可改变,但是切片的长度是可以改变的,而已追加元素,在追加时可能使切片的容量增大
var identifier []type //声明一个未指定大小的数组来定义切片,不建议使用 //或者使用make()函数来创建切片 var slice1 []type = make([]type,len) slice1 := make([]type,len) //也可以指定容量,其中capacity为可选参数 make([]T,length,capacity)
切片就是没有指定长度的数组
获取数组中的值
b := a[:] //获取数组中的所有值,这也是基于数组的切片定义,此时b是一个切片而不是数组,b没有长度限制
- 获取切片的值是前包后不包的,即 [1:4] 的取值实际上是1~3
获取切片的长度和容量
- len()函数来获取切片的长度
- cap()函数来获取切片的容量
- 切片容量是从它的第一个元素开始数,到其底层数组元素末尾的个数,如果是基于数组的,就是到其数组的最后一个位置,即使限定了最后的下标[,end]
- 切片的长度就是它所包含的元素个数
用make函数创建切片
make([]T,len,cap)
- 第一个参数是类型,第二个参数是长度,第三个长度是容量
切片扩容
- 使用go语言内置函数,append()可以为切片动态添加元素,每个切片会指向一个底层数组
- append()等号前写用哪个变量去接收,append()的第一个参数写地址,第二个参数是添加的值
切片合并
append(sliceA,sliceB…)
第一个参数是加到哪里,第二个参数是用什么加,第二个参数后面会自动加上 …
sliceA := []string{"php","java"}
sliceB := []string{"nodejs","go"}
sliceA = append(sliceA,sliceB...) //注意这个地方是重新赋值,而不需要声明,因此不用 := ,用 = ,另外,要在用于扩容的数组后面加上三个点
fmt.Println(sliceA)
- 上面这个例子是将sliceB合并到sliceA的后面,然后重新赋值给sliceA
切片是一种引用类型,如果想要不同时改变,则使用copy()函数
copy()函数是赋值,而不会发生引用
即使用copy()函数不会同时修改sliceA和sliceB
package main import "fmt" func main() { sliceA := []int{1,2,3,4,5} sliceB := make([]int,5,6) //第一个参数是被赋值,第二个参数是用什么去赋值,copy(被赋值,用什么去赋值); copy(sliceB,sliceA) fmt.Println(sliceA) fmt.Println(sliceB) }
删除切片中的元素
a := []int{1,2,3,4,5}
a = append(a[:2],a[3:]...)
//和链表的删除操作思路相同,将一个切片分成两段,前一段和后一段之间是要shan'ch
指针
声明
var var_name *var_type
var ip *int
var fp *float32
指针数组
package main
import "fmt"
const MAX int = 4
func main() {
a := []int{0,1,2,3}
var i int
var ptr [MAX]*int
for i = 0;i < MAX;i++ {
ptr[i] = &a[i]
}
for i = 0;i < MAX; i++ {
fmt.Printf("a[%d] = %d",i,*ptr[i])
}
}
指针作为函数的参数
package main
import "fmt"
func main() {
var a int = 100
var b int = 200
swap(&a,&b)
fmt.Println(a,b)
}
func swap(x *int,y *int) {
*x,*y = *y,*x
}
结构体
定义结构体
type struct_variable_type struct {
member definition
member definition
}
忽略的字段将为0或者为空
也可以使用 key=>value 的形式,使用键值对的形式可以不用按照顺序去定义
Books{title: "",author: "",subject: "",book_id: ""}
结构体作为函数的参数
func printBook(book Books) {
fmt.Printf(book.title)
}
func name(var_name var_type) {
fmt.Printf(var_name.value)
}
结构体指针
var struct_pointer *Books
//使用指针变量存储结构体变量的地址
struct_pointer = &Book1
//使用结构体指针访问结构体成员
struct_pointer.title
范围range
range用于for循环中迭代数组,切片,通道,集合元素
数组和切片中它返回元素的索引和对应的值,在集合中返回key-value对
for key,value := range oldMap { newMap[key] = value }
for循环的range可以省略key和value
package main import "fmt" func main() { map1 := make(map[int]float32) map1[1] = 1.0 map1[2] = 2.0 for key,value := range map1 { fmt.Printf("key: %d,value %f",key,value) } for key := range map1 { fmt.Printf("key is: %d",key) } for _,value := range map1 { fmt.Printf("value is: %f",value) } }
map集合
无序的键值对的集合
可以通过key来快速地检索数据,类似于索引,指向数据的值
map是一种集合,可以进行迭代,但是是无序的,无法决定它的返回顺序,因为map是通过hash来实现的
package main import "fmt" func main() { var countryCapitalMap map[string]string //map的声明: map[键的类型]值的类型 countryCapitalMap = make(map[string]string) countryCapitalMap ["france"] = "巴黎" countryCapitalMap [ "Italy" ] = "罗马" countryCapitalMap [ "Japan" ] = "东京" countryCapitalMap [ "India " ] = "新德里" for country := range countryCapitalMap { fmt.Println(country) } }
delete函数
delete函数用于删除集合的元素,参数为map和其对应的key
delete(map_name,key) delete(map_name,key)
package main import "fmt" func main() { countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"} fmt.Println("原始地图") for country := range countryCapitalMap { fmt.Println(country,"首都是",countryCapitalMap[country]) } delete(countryCapitalMap,"France") fmt.Println() for country := range countryCapitalMap { fmt.Println(country,"首都是",countryCapitalMap[country]) } }
go语言递归
递归,在运行的过程中调用自己
func recursion() { recursion() } func main() { recursion() }
package main import "fmt" func fib(n int) int{ if n < 2 { return n } return fib(n-2)*(n-1) } func main() { var i int for i = 0;i < 10;i++ { fmt.Printf("%d\t",fib(i)) } } //斐波那契数列
go语言类型转换
type_name(expression)
//expression中填变量的名字
package main
import "fmt"
func main() {
var sum int = 17
var count int = 5
var mean float32
mean = float32(sum)/float32(count)
fmt.Printf("mean的值为 %f",mean)
}
- go不支持隐式类型转换,必须进行显示类型转换,否则会报错
go语言接口
将所有具有共性的方法定义在一起,任何其它类型只要实现了这些(全部)方法就是实现了这个接口
type interface_name interface { method_name1 [return_type] method_name2 [return_type] } func (struct_name_variable struct_name) method_name1() [return_type]}{ //方法实现 }
package main import "fmt" type Phone interface { call() } type NokiaPhone struct { } func (nokiaPhone NokiaPhone) call() { fmt.Println("abc") } type IPhone struct { } func (iphone IPhone) call() { fmt.Println("def") } func main() { var phone Phone phone = new(NokiaPhone) phone.call() phone = new(IPhone) phone.call() }
go并发
go语言支持并发,只需要通过关键字go来开启goroutine即可
goroutine是轻量级线程,goroutine的调度是由golang运行时进行管理的
go 函数名(参数列表) go f(x,y,z)
go允许使用go语句开启一个新的运行期线程,即goroutine,以一个不同的,新创建的goroutine来执行一个函数
同一个程序中所有的goroutine共享同一个地址空间
package main import ( "fmt" "time" ) func say(s string) { for i := 0;i < 5;i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("world") say("hello") } //这个程序中输出的hello和world没有固定的顺序,因为是两个goroutine在执行
通道
channel是用来传输数据的一个数据结构
通道可以用于两个goroutine之间通过传递一个指定类型的值来同步运行和通讯
操作符 <- 用于指定通道的方向,发送或接收,如果没有指定方向,则为双向通道
ch <- v //把v发送到通道ch v := <- ch //从ch接收数据,并把值赋给v
通道的声明
ch := make(chan int)
//通道的声明使用chan关键字,在声明之前必须先创建
默认情况下,通道是不带缓冲区的,发送端发送数据,同时必须有接收端相应的接收数据
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 fmt.Println(x,y,x+y) }
通道缓冲区
ch := make(chan int,100)
//通道可以设置缓冲区,通过make的第二个参数指定缓冲区的大小
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区中,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据
不过由于缓冲区的大小是有限的,还是需要有接收端来接收数据,否则缓冲区一满,数据发送端就无法再发送数据了
const和iota
和定义变量类似,就是将定义变量时的var关键字替换为const关键字
package main import "fmt" func main() { const length int = 10 fmt.Println("length = ",length) }
一个函数有多个返回值
package main
import "fmt"
//返回多个返回值,匿名的
func foo(a string,b int) (string , int) {
fmt.Println("a=",a)
fmt.Println("b=",b)
return a,b
}
//返回多个返回值,有形参名称的
func foo3(a string,b int) (r1 int,r2 int) {
fmt.Println("-----foo3-----")
fmt.Println("a= ",a)
fmt.Println("b= ",b)
//给有名称的返回值变量赋值
r1 = 1000
r2 = 2000
return r1,r2
}
func foo4(a string,b int) (r1,r2 int) {
fmt.Println("-----foo4------")
fmt.Println("a= ",a)
fmt.Println("b= ",b)
//给有名称的返回值变量赋值
r1=1000
r2=2000
return r1,r2
}
func main() {
ret1 ,ret2 := foo("abc",100);
fmt.Println("ret1:",ret1,"ret2:",ret2)
ret3 , ret4 := foo3("foo3",333)
fmt.Println("ret3 = ",ret3,"ret4 = ",ret4)
ret5,ret6 := foo
}
导包的路径问题和init方法调用
import匿名及别名导包方式
匿名导包
想调用这个包的init()函数,但是不想使用这个包的接口的需求
这种需求就可以使用匿名导入方式**(这种方法类似于起别名)**
就可以在导入的包的前面加上下划线,空格
import _ "GolangStudy/S-init/lib1"
此时无法使用当前包的方法,但是会执行当前包内部的init方法
别名导包
在包的名字前面加上包的别名,即可使用别名,不使用包原来的名字
mylib2 "GolangStudy/S-init/lib2" //在main函数中可以使用别名进行调用 mylib2.Lib2Test();
在包的名字前面加上点,空格,就可以不使用包的名字,直接使用包内部的函数
. "GolangStudy/S-init/lib2" Lib2Test();
这种点的导入方式尽量不要使用,可能会有同名函数起冲突
go语言的析构函数
go语言的析构函数关键字是defer
defer fmt.Println("一个函数体中允许有多个析构函数,调用顺序是从上到下"); //这样会在函数体的结束调用这个析构函数 defer fmt.Println();
在函数的前面添加defer关键字即为析构函数
defer和return的调用先后问题
- defer是当前函数的生命周期全部结束之后才会被调用,才会出栈
- 调用顺序是先调用return,在函数的生命周期结束之后才会调用defer
- 书写顺序是先书写defer,然后再书写return,类似于c++
给一个结构体绑定方法
给一个结构体绑定方法,一定要用指针
func (this *Hero) Show(){}
go语言方法
方法是作用在指定的数据类型上的,和指定的数据类型绑定,因此自定义类型都可以拥有方法
type A struct { Num int; } func (a A) test(){ //中间的(a A)表示这个方法是绑定到A结构体上的,类似于成员方法(A这个类的成员方法) fmt.Println(a.Num); }
这个方法是和某个对象绑定的,所以通过某个对象来调用
package main import ( "fmt" ) type Person struct { Name string } func (p Person) test() { //这个方法是绑定到Person类的 fmt.Printf("test() name=%s",p.Name) } func main() { var p Person p.Name = "tom" p.test() //所以要通过Person类的实例进行调用,不能直接test()直接调用,也不能使用其它类型的变量来调用 }
func (p Person) test(){} …p表示哪个Person变量调用,这个p就是它的副本,这点和函数传参(引用)非常相似
p这个名字,由程序员指定,不是固定
go语言错误处理
通过内置的错误接口提供了简单的错误处理机制
error类型是一个接口类型
type error interface { Error() string }
可以在编码中通过实现error接口类型来生成错误信息
函数通常在最后的返回值中返回错误信息,使用errors.New可以返回一个错误信息
func Sqrt(f float64) (float64,error) { if f < 0 { return 0,errors.New("math:square root of negative number") } }
go语言创建对象的方式
使用T{…}方式,结果为值类型
使用new的方式,结果为指针类型
使用&方式,结果为指针类型
c3 := &Car{ color:"红色" length:"10" }//使用&进行对象的创建,使用冒号进行赋值
go语言高并发
通道
通道channel是用来传递数据的一个数据结构
可以用于两个goroutine之间通过传递一个指定类型的值来实现同步运行和通讯,<-用来指定通道的方向,发送或接受数据,如果没有指定方向,则为双向通道
声明一个通道使用chan关键字,通道在使用前必须先创建
ch := make(chan int)
复习
channel定义
channel : make(chan 类型,容量)
ch := make(chan string)
写端:ch <- “hehe” ,写端写数据,读端不再读,写端阻塞
读端:str := <- ch , 读端读数据,同时写端不在写,读端阻塞
通道中的数据只能读取一次,不能多次重复读取,读完就消失
有缓冲通道:同步通信
无缓冲通道:异步通信
ch := make(chan int,5)
- len(ch):channel中剩余未读取数据的个数
- cap(ch):通道的容量
关闭channel
写和读是两个不同的goroutine,当确定不再继续向对端发送数据时,关闭channel,使用close(ch)关闭channel
如果写端关闭,读端再去读,就会读到一个0/nil,类似于EOF,此时就需要去关闭channel
对端可以判断channel是否关闭
if num,ok := <- ch;ok == true{ //如果对端已经关闭,ok==false,num无数据 //如果对端没有关闭,ok==true,num保存读到的数据 }
可以使用range来替代ok
for num := range ch { //和循环遍历类似,ch不能替换为<- }
关闭的channel不能再向其中写数据,但是可以从中读取数据
生产者消费者模型
-
package main import "fmt" func producer(out chan <- int,a int) { out <- a fmt.Println("生产者提供数据",a) close(out) } func consumer(in <- chan int){ for data := range in{ fmt.Println("消费者得到数据",data) } } func main() { ch := make(chan int,5) go producer(ch,5) consumer(ch) } //双向channel var ch chan int 没有箭头 //单向写channel var sendCh chan <- int sendCh = make(chan <- int) 可以理解为chan为写入对象或读出对象,写入channel chan <- 读出channel <- chan //单向读channel var recvCh <- chan int recvCh = make(<- chan int) //双向channel可以任意转换为一种单向channel //单向channel不能转换为双向