Go基本语法
- 由于笔者已经对C/C++有所了解,所以下面的笔记将会从比较Go和C/C++的差异入手,相同部分便不再赘述
- 教材源于菜鸟教程,但有所修改
环境搭建
Windows
到官网上下载Windows的安装文件后安装即可。
Hello World
创建文件hello.go
,然后输入如下内容
package main
import "fmt"
func main() {
/* first exsample*/
fmt.Println("Hello, World!")
}
运行命令
go run hello.go
生成二进制文件命令
go build hello.go
然后在控制台即可看到输出。
下面是程序各个部分的含义。
package main
。定义包名。我们必须在源文件中的非注释第一行指明文件属于的包。package main
表明这是一个可独立执行的程序,每个Go应用程序都包含一个名为main
的包。import "fmt"
。和C++中的#include <iostream>
差不多,fmt包中实现了格式化输入输出函数。func main(){...}
。程序开始执行的函数。main函数是每一个可执行程序所必须包含的。一般来说是启动后第一个执行的函数。/*...*/
。注释。亦可以使用//
来注释单行。fmt.Println(...)
将字符串输出到控制台并自动添加\n
。- 当标识符以大写字母开头时,表示这个标识符所指示的对象可以被外部包的代码所使用;若是以小写字母开头,则对包外是不可见的,但在包内是可见的并且是可用的。
{
不能单独放在一行,否则会产生错误。- Go程序中,一行代表一个语句的结束,所以结尾可以不加
;
号,但多条语句写在一行时需要加上;
号。
数据类型
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。
// 布尔型
bool
// 数字类型,u表示无符号,数字表示所占位数
uint8
uint16
uint32
uint64
int8
int16
int32
int64
// 浮点型
float32
float64
complex64
complex128
// 其他类型
byte // uint8
rune // int32
uint // uint32 or uint64
int // uint
uintptr // 无符号整型,用于存放一个指针
string // 字符串类型
// 派生类型
struct
func
interface
map
变量
这里不区分“声明”和“定义”的区别,但这两者在C/C++确是不同的概念。
概述
Go的变量由字母、数字和下划线组成,首字母不能为数字。变量的声明一般如下。
var identifier type
例如
var a string = "Hello World"
var b int = 10
也可以一次声明多个变量
var identifier_1, identifier_2 type
例如
var b, c int = 1, 2
特殊的声明方式
声明但未初始化
var a int
当变量未初始化时,其会被赋值为默认值,如下所示。
- 数值类型=0
- 布尔类型=false
- 字符串=“”
- 其余=nil(nil相当于Null或nullptr)
缺省类型的声明
Go可以在声明时缺省类型,其类型根据定义时所赋的值的类型判断,例如。
var a = true
此时a的类型为bool
缺省var和类型的声明
例如
intVal := 1
其中,intVal
必须是未被声明的标识符,否则会发生编译错误。
也可以多个变量一起声明
intVal1, intVal2 := 1, 2
多变量赋值
多个变量可以在一条语句中赋值,=
左边的值对应于=
右边相同位置的值。
intVal1, intVal2 = 1, 2
此时,可以方便地互换两个变量的值。
a, b = b, a
也可以使用抛弃值_
,用来充当占位功能。
_, variable = 1, 2
作用域
Go语言的变量的作用域与C/C++相同。
类型转换
强制转换的语法如下
type_name(expression)
例如
sum := 17
count := 5
mean := float32(sum) / float32(count)
Go不支持隐式类型转换。
常量
定义
常量是一个简单的标识符,表明这个变量在程序运行时不会被修改。常量中的数据类型只可以是布尔型、数字型和字符串型,其定义如下所示。
const identifier [type] = value
[type]
表示在定义时变量的类型可以被省略,如下所示。
const a string = "abc"
const b = "abc"
const c, d = "abc", "dce"
常量可以用作枚举
const (
a = "abc"
b = len(a)
c = unsafe.Sizeof(a)
)
itoa
itoa
是一个特殊的常量,可认为是编译器修改的常量。
itoa
在const
关键字第一次出现时被置为0,const中每增加一行常量声明在加1(无论itoa
是否被赋值)。因此,itoa
可认为是const
语句块中的行索引。
const (
a = itoa // 0
b // 1
c // 2
d = "a"
e // "a"
f = 100
g // 100
h = itoa // 7
i // 8
)
运算符
Go的运算符基本和C/C++相同,主要增加/修改/删除了以下运算符。
%=
<<=
>>=
&=
^=
|=
>> // 只有逻辑右移
// 没有三目运算符 ? :
条件语句
单if语句
语法如下。
if bool_condition {
}
例如
if a < 20 {
fmt.Println("a is less than 20")
}
if…else语句
语法如下。
if bool_condition {
} else {
}
例如。
if a < 20 {
fmt.Println("a is less than 20")
} else {
fmt.Println("a is larger than 20")
}
else if语句
Go中没有else if语句,所以使用if嵌套来实现对应的功能。
Switch语句
一般的switch语句
Go中的Switch语句的执行过程从上到下,直到找到匹配项。执行完匹配项后switch语句便结束了,不会像C/C++一样在不加break时会顺序执行接下来的语句。所以,switch中的case可以不加break,若想一旦一个case匹配成功后其后的case,可以在每个case的最后加上fallthrough
。一般的Switch语句的语法如下
switch var1 {
case val1:
...
case val2:
...
case val3, val3:
...
default:
...
}
实际上,var1可以是任何类型,不必像C/C++一样必须指定为整型。同时,case中的值必须和var1同类型。
Type Switch
switch语句的变量可以是变量的类型,例如
switch x.(type){
case type:
...
case type:
...
default:
...
}
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("未知型")
循环语句
Go中没有while循环,所有的循环都使用for来完成。
For形式1
for init; condition; post{
...
}
意义和C/C++相同
for i := 0; i < 10; i++ {
...
}
For形式2
for condition{
...
}
相当于C/C++的while语句
for ; sum <= 10; {
sum++
}
但可以简化为
for sum <= 10 {
...
}
For形式3
for {
...
}
上面的循环是一个无限循环,等价于C/C++中的
while(1) {
...
}
For形式4
for的第四种形式是for-each-range循环,将在后面讨论。
函数
函数是最基本的代码块,用于执行一个任务。Go语言最少有一个main函数。
定义
Go中函数定义如下
func function_name([parameter list]) [return_types] {
...
}
各个部分的含义如下。
func
:函数声明。function_name
:函数名称。函数名称和参数列表构成了函数签名。parammeter_list
:参数列表。其中,参数列表是可选的。return_types
返回类型,函数返回一列值(不限于1个返回值)。没有值返回时可以省略。
例如
// 有返回值和参数
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int
if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}
// 无返回值和参数
func main() {
}
// 多值返回
func swap(x, y string) (string, string) {
return y, x
}
但函数返回多个值时,有时我们并不希望取其所有值,但在caller
中(调用函数的地方)返回值的赋值又是按顺序的,所以在caller
中使用占位符_
即可。
Go中函数参数的传递依然有按值传递和按应用传递。但按引用传递时传递的是指针,Go没有C++中的引用类型。
函数变量
在Go中函数可以像变量一样被定义和使用。
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
因此,函数可以作为形参被传递到另外一个函数中,相当于C++中的仿函数。
package main
import "fmt"
// 声明一个函数类型
type cb func(int) int
func main() {
testCallBack(1, callBack)
testCallBack(2, func(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
})
}
func testCallBack(x int, f cb) {
f(x)
}
func callBack(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
}
闭包
Go支持匿名函数,可作为闭包。也就是说我们可以在Go的函数中定义函数,这个函数中的函数可以直接使用定义它的函数中的变量,不必在其形参中写出。
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}
方法
Go中同时有函数和方法。一个方法是一个包含了接收者的函数,接收者可以是命名类型和结构以类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。
方法相当于C++中class
的成员函数,但Go的方法的意义显然比成员函数要广得多。
方法定义如下
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
例如
type Circle struct {
radius float64
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}
在方法中的variable_data_type
也影响着我们是否可以在方法中改变其属于的变量的中值。
例如下面这个方法不会改变c内部的radius的值。
func (c Circle) changeRadius(radius float64) {
c.radius = radius
}
因为c是按值传递的,因此,我们需要按引用传递才可以改变c内部的成员的值。
// 注意如果想要更改成功c的值,这里需要传指针
func (c *Circle) changeRadius(radius float64) {
c.radius = radius
}
数组
Go中的数组是一个具有唯一类型且长度固定的数据项序列,并且数组下标从0开始。
声明
数组的声明语法格式如下。
var array_name [SIZE] variable_type
例如
var balance [10] float32
我们可以在数组声明时初始化
var balance = [3] float32 {1000.0, 2.0, 3,4}
其中,{}
中的数据项数量不可以大于[]
指定的数量。当然,我们也可以不在[]
指定数据项的数量,而是通过{}
中的数据项的数量来确定。
访问
我们可以通过索引来访问数组的元素。
salary := balance[2]
多维数组
Go支持多维数组,定义语法如下。
var variable_name [d_1][d_2]...[d_n] variable_type
例如,一个三维数组的声明
var threedim [5][10][4] int
向函数传递数组
向函数传递数组有以下两种方式
func myFunction(param [10]int) {
...
}
// 此时并不知道param的元素个数,需要再将其元素个数增加为另一个形参
func myFunction(param []int) {
...
}
指针
定义
指针的定义语法如下。
var var_name *var-type
用法与C/C++大致相同。空指针使用nil
来标识。
指针数组
指针数组表示数组存放的元素是指针,定义语法如下。
var ptr [MAX] *int
指针的指针
指针的指针定义语法如下。
var ptr **int
结构体 (struct)
定义
结构体的声明语法如下。
type struct_variable_type struct {
member definition
member definition
...
member definition
}
我们可以定义结构体并初始化,语法如下。
// 初始化的第一种方式
variable_name := struct_variable_type {value1, value2...valuen}
// 初始化的第二种方式
variable_name := struct_variable_type { key1: value1, key2: value2..., keyn: valuen}
例如
// 创建一个新的结构体
fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})
// 也可以使用 key => value 格式
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})
// 忽略的字段为 0 或 空
fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
访问成员变量
使用结构体变量和使用结构体变量的指针访问结构体变量的语法是相同的,即
结构体.成员名
C++中使用结构体变量指针时需要使用
->
来访问变量。
切片(Slice)
切片是对数组的抽象,与数组相比,切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大,相当于C++中的std::vector
。
定义
通过声明一个未指定大小的数组来定义空切片
var identifier [] type
也可以通过make来指定定义时切片的大小。
var slice1 []type = make([]type, len)
// 或者指定capacity
var slice1 []type = make([]T, length, capacity)
初始化
s := [] int {1,2,3}
其中,cap(s)
=len(s)
=3。
我们也可以使用一个已有的切片来初始化切片。
// 初始化切片s为arr的引用
s := arr[:]
// 从下标startIndex~endIndex的元素创建切片
s := arr[startIndex:endIndex]
// 从下标startIndex~len(s)-1的元素创建切片
s := arr[startIndex:]
// 从下标0~endIndex的元素创建切片
s := arr[:endIndex]
len函数和cap函数
len函数用来获取切片的长度,cap函数用来获取切片的容量,例如
// s是切片
len(s)
cap(s)
切片截取
我们可以通过指定切片的上下限来获取切片的子切片,参考上面的初始化。
append函数
append函数可以用来为切片追加元素,其定义如下。
func append(s []T, vs ...T) []T
例如
var numbers [] int
// 使用append追加3个元素, 0, 512, 564
append(numbers, 0)
append(numbers, 512)
append(numbers, 563)
copy函数
copy函数可以将一个Slice的内容复制到另一个Slice,函数定义如下。
copy(dst_slice, src_slice)
其中,要求len(dst_slice) >= len(src_slice)
。
范围(range)
range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对,例如。
numbers := [] int {0, 1, 2}
for i, num := range numbers {
...
}
集合(map)
定义
map的声明语句如下。
var map_variable map[key_data_type]value_data_type
上面的map是一个nil map,不能存放键值对,需要使用make来初始化。
map_variable = make(map[key_data_type])value_data_type
添加或修改map中的元素
使用[]
即可添加或修改map中的元素,例如
aMap = make(map[string]string)
// insert
aMap["this is a key"] = "a value"
// update
aMap["this is a key"] = "update balue"
引用map中的元素
使用[]
即可引用map中的元素,例如
someValue := aMap["this is a key"]
删除map中的元素
使用delete函数即可删除map中的元素,用法如下
delete(map, key)
例如
delete(aMap, "this is a key")
遍历map中的元素
使用range即可在for中遍历map的元素,例如。
for key, value := range(aMap) {
...
}
接口
接口是所有方法的一个集合,任何其他类型只要实现了这些方法就是实现了这个接口。
接口的定义与实现如下。
/* 定义接口 */
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] {
/* 方法实现*/
}
错误处理
Go通过内置的错误接口error
提供了简单的错误处理机制。
其定义如下。
type error interface {
Error() string
}
使用方法如下。
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}
比较返回值中的error项是否为0即可判断是否出现错误。
并发
goroutine
goroutine的语法格式如下。
go function_name ([parameter_list])
一旦父线程执行完,则其通过go创建的线程也会退出。
通道(Channel)
通道是用来传递数据的数据结构,可用于两个goroutine之间传递一个指定类型的值来同步运行和通信。
操作符<-
用于指定通道的方向,发送和接收,若未指定方向,则为双向通道。
ch <- v // 把 v 发送到通道 ch
v := <-ch // 从 ch 接收数据
// 并把值赋给 v
使用chann关键字声明一个通道,通道在使用前必须使用make来创建。
ch := make(chan int)
默认情况下,通道是不带缓冲区的。因此,当发送端发送数据时,必须有接收端接收数据。
通道可以在make的时候设置缓冲区大小。
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
通道和缓冲的2种场景如下。
-
无缓冲的通道,发送方会阻塞直到接收方从通道中接收了值。
-
有缓冲的通道,发送方则会阻塞直到发送的值被拷贝到缓冲区内。如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值,即阻塞等待到缓冲区不满。
无论通道是否有缓冲,接收方在通道有值之前会一直阻塞。
通道的遍历的时候使用通道的第二个参数来判断结束,语法如下
v, ok := <-ch
如果ok
=false
表明通道已经关闭,代表之后没有数据可以读。
通道的关闭使用close()
,如果通道不关闭,则接收方将会一直等待通道数据而无法结束。
select
select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个Channel通信操作<-
,要么是发送要么是接收,如下所示。
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。