Golang基础语法【原创】

本文基于GO官网上的例子写就,主要帮助自己在学习go语言的过程中记忆,更多的是自己对其中一些概念的理解。

1.1 Go基础语法

函数

函数声明关键字func,包括函数名、参数列表(参数名 参数类型)、函数返回值,(需要指明返回类型,可以指定返回变量名)。具体格式如下:

1
2
3
4
func funcName(a , b int) (sum int){
sum = a + b
return sum
}

基本数据类型

int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64、 uintptr 、byte(int8)、rune(int32)、float32、float64、complex64、complex128、 string、 bool

int,uint 和 uintptr 类型在32位的系统上一般是32位,而在64位系统上是64位。当你需要使用一个整数类型时,你应该首选 int,仅当有特别的理由才使用定长整数类型或者无符号整数类型。

变量

变量声明采用var关键字,如果声明时指定了初始值,则可以不写变量类型:

1
var name string = "xxx"

或短变量声明,在函数中, := 简洁赋值语句在明确类型的地方,可以用于替代 var 定义。函数外的每个语句都必须以关键字开始( var 、 func 、等等), := 结构不能使用在函数外。

1
name := "xxx"

var 语句定义了一个变量的列表;跟函数的参数列表一样,类型在后面。 var 语句可以定义在包或函数级别。

循环for

Go中只有一种for循环,基本的 for 循环包含三个由分号分开的组成部分:

  • 初始化语句:在第一次循环执行前被执行
  • 循环条件表达式:每轮迭代开始前被求值
  • 后置语句:每轮迭代后被执行

且循环条件不能有圆括号,循环体需要大括号,其语法如下:

1
2
3
4
sum :=0
for i:=0; i<100; i++ {
sum++
}

如果前置条件、后置条件和分号都去掉,这就退化为C语言中的while循环了,语法如下:

1
2
3
4
sum := 0;
for sum < 100 {
sum++
}

条件if

Go中的if语句同循环一样,不需要圆括号,具体执行语句需要大括号,其语法如下:

1
2
3
if i < 10 {
//todo something
}

if 的便捷语句

跟 for 一样, if 语句可以在条件之前执行一个简单语句。

由这个语句定义的变量的作用域仅在 if 范围之内。包括在 if 的便捷语句定义的变量同样可以在任何对应的 else 块中使用。

包的定义

package是golang最基本的分发单位和工程管理中依赖关系的体现。每个golang源代码文件开头都拥有一个package声明,表示该golang代码所属的package。

要生成golang可执行程序,必须建立一个名为main的package,并且在该package中必须包含一个名为main()的函数。

在golang工程中,同一个路径下只能存在一个package,一个package可以拆成多个源文件组成

按照惯例,包名与导入路径的最后一个目录一致。

包的导入

包的导入使用import关键字

例如:

1
2
3
4
5
6
import "fmt"
//或者使用打包的导入语句
import (
"fmt"
"math"
)

导出名

在 Go 中,首字母大写的名称是被导出的。

在导入包之后,你只能访问包所导出的名字,任何未导出的名字是不能被包外的代码访问的。

Foo 和 FOO 都是被导出的名称。名称 foo 是不会被导出的。

零值

变量在定义时没有明确的初始化时会赋值为零值 。

零值是:

  1. 数值类型为 0 ,
  2. 布尔类型为 false ,
  3. 字符串为 “” (空字符串)。

类型转换

表达式 T(v) 将值 v 转换为类型 T 。

一些关于数值的转换例子:

1
2
3
4
5
6
7
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
//或者,更加简单的形式:
i := 42
f := float64(i)
u := uint(f)

与 C 不同的是 Go 的在不同类型之间的赋值时需要显式转换。

常量

常量的定义与变量类似,只不过使用 const 关键字。

常量可以是字符、字符串、布尔或数字类型的值。

常量不能使用 := 语法定义。

switch

你可能已经知道 switch 语句会长什么样了。

除非以 fallthrough 语句结束,否则分支会自动终止。

switch 的条件从上到下的执行,当匹配成功的时候停止。

没有条件的 switch

没有条件的 switch 同 switch true 一样。

这一构造使得可以用更清晰的形式来编写长的 if-then-else 链。

defer

defer 语句会延迟函数的执行直到上层函数返回。

延迟调用的参数会立刻生成,但是在上层函数返回前函数都不会被调用。

defer栈

延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。

指针

Go 具有指针。 指针保存了变量的内存地址。

类型 *T 是指向类型 T 的值的指针。其零值是 nil 。& 符号会生成一个指向其作用对象的指针。

1
2
3
var p *int
i := 42
p = &i
  • 符号表示指针指向的底层的值。
1
2
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i

这也就是通常所说的“间接引用”或“非直接引用”。

与 C 不同,Go 没有指针运算。

结构体

一个结构体( struct )就是一个字段的集合。

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
type Vertex struct {
X int
Y int
}

func main() {
fmt.Println(Vertex{1, 2})
}

结构体字段使用点号来访问。

结构体字段可以通过结构体指针来访问。

通过指针间接的访问是透明的。

结构体文法

结构体文法表示通过结构体字段的值作为列表来新分配一个结构体。

使用 Name: 语法可以仅列出部分字段。(字段名的顺序无关。)

特殊的前缀 & 返回一个指向结构体的指针。

数组

类型 [n]T 是一个有 n 个类型为 T 的值的数组。

下面表达式定义变量 a 是一个有十个整数的数组。

1
var a [10]int

数组的长度是其类型的一部分,因此数组不能改变大小。 这看起来是一个制约,但是请不要担心; Go 提供了更加便利的方式来使用数组。

slice

一个 slice 会指向一个序列的值,并且包含了长度信息。

[]T 是一个元素类型为 T 的 slice。

len(s) 返回 slice s 的长度。

slice 可以包含任意的类型,包括另一个 slice。

对 slice 切片

slice 可以重新切片,创建一个新的 slice 值指向相同的数组。

如表达式:s[lo:hi],表示从 lo 到 hi-1 的 slice 元素,含前端,不包含后端。因此 s[lo:lo]是空的,而s[lo:lo+1]有一个元素。

构造 slice

slice 由函数 make 创建。这会分配一个全是零值的数组并且返回一个 slice 指向这个数组:

1
a := make([]int, 5)  // len(a)=5

为了指定容量,可传递第三个参数到 make:

1
2
3
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4

slice 的零值是 nil

一个 nil 的 slice 的长度和容量是 0。

向 slice 添加元素

向 slice 的末尾添加元素是一种常见的操作,因此 Go 提供了一个内建函数 append 。 内建函数的文档对 append 有详细介绍。

1
func append(s []T, vs ...T) []T

append 的第一个参数 s 是一个元素类型为 T 的 slice ,其余类型为 T 的值将会附加到该 slice 的末尾。

append 的结果是一个包含原 slice 所有元素加上新添加的元素的 slice。

如果 s 的底层数组太小,而不能容纳所有值时,会分配一个更大的数组。 返回的 slice 会指向这个新分配的数组。

range

for 循环的 range 格式可以对 slice 或者 map 进行迭代循环。

当使用 for 循环遍历一个 slice 时,每次迭代 range 将返回两个值。 第一个是当前下标(序号),第二个是该下标所对应元素的一个拷贝。

可以通过赋值给 _ 来忽略序号和值。

如果只需要索引值,去掉 “ , value ” 的部分即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
pow := make([]int, 10)
for i := range pow {
pow[i] = 1 << uint(i)
}
for _, value := range pow {
fmt.Printf("%d\n", value)
}
}

map

map 映射键到值。

map 在使用之前必须用 make 来创建;

值为 nil 的 map 是空的,并且不能对其赋值。

map 的文法跟结构体文法相似,不过必须有键名。

若顶级类型只是一个类型名,你可以在文法的元素中省略它。

修改 map

在 map m 中插入或修改一个元素:

1
2
var m = make(map[string]string)
m["name"] = "Golang"

获得元素:

1
name := m["name"]

删除元素:

1
delete(m, "name")

通过双赋值检测某个键存在:

1
name, ok = m["name"]

如果 name 在 m 中, ok 为 true。否则, ok 为 false,并且 name 是 map 的元素类型的零值。

同样的,当从 map 中读取某个不存在的键时,结果是 map 的元素类型的零值。

函数值

函数也是值。他们可以像其他值一样传递,比如,函数值可以作为函数的参数或者返回值。

函数的闭包

Go 函数可以是一个闭包。闭包是一个函数值,它引用了函数体之外的变量。 这个函数可以对这个引用的变量进行访问和赋值;换句话说这个函数被“绑定”在这个变量上。

例如,函数 adder 返回一个闭包。每个返回的闭包都被绑定到其各自的 sum 变量上。

练习:斐波纳契闭包

现在来通过函数做些有趣的事情。

实现一个 fibonacci 函数,返回一个函数(一个闭包)可以返回连续的斐波纳契数。

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
package main

import "fmt"

// fibonacci 函数会返回一个返回 int 的函数。
func fibonacci() func() int {
first := 0
second := 0
index :=0

return func() int{
sum := first + second
switch index {
case 0:
first = 0
second = 1
case 1:
first = 0
second = 1
default:
first = second
second = sum
}
index++
return sum
}
}

func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Printf("[%v]=>%v\n",i,f())
}
}

方法

Go 没有类。然而,仍然可以在结构体类型上定义方法。

方法接收者 出现在 func 关键字和方法名之间的参数中。格式为:

1
2
3
func (变量 类型) funcName(param1 type1,param2 type2)(return_param, return_type) {
//xxx
}

你可以对包中的 任意 类型定义任意方法,而不仅仅是针对结构体。

但是,不能对来自其他包的类型或基础类型定义方法。

接收者为指针的方法

方法可以与命名类型或命名类型的指针关联。

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
package main

import (
"fmt"
"math"
)

type Vertex struct {
X, Y float64
}

func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
return math.Sqrt(v.Xv.X + v.Yv.Y)
}

func main() {
v := &Vertex{3, 4}
fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
v.Scale(5)
fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

刚刚看到的两个 Abs 方法。一个是在 *Vertex 指针类型上,而另一个在 MyFloat 值类型上。 有两个原因需要使用指针接收者。首先避免在每个方法调用中拷贝值(如果值类型是大的结构体的话会更有效率)。其次,方法可以修改接收者指向的值。

尝试修改 Abs 的定义,同时 Scale 方法使用 Vertex 代替 *Vertex 作为接收者。

当 v 是 Vertex 的时候 Scale 方法没有任何作用。Scale 修改 v。当 v 是一个值(非指针),方法看到的是 Vertex 的副本,并且无法修改原始值。

Abs 的工作方式是一样的。只不过,仅仅读取 v。所以读取的是原始值(通过指针)还是那个值的副本并没有关系。

接口

接口类型是由一组方法定义的集合。

接口类型的值可以存放实现这些方法的任何值

接口定义

1
2
3
type Abser interface {
Abs() float64
}

接口实现

1
2
3
4
5
6
7
8
type MyFloat float64

func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}

隐式接口

类型通过实现接口中的方法来实现接口。 没有显式声明的必要;所以也就没有关键字“implement“。

隐式接口解藕了实现接口的包和定义接口的包:互不依赖。

因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。

Stringers

一个普遍存在的接口是 fmt 包中定义的 Stringer

1
2
3
type Stringer interface {
String() string
}

Stringer 是一个可以用字符串描述自己的类型。fmt包 (还有许多其他包)使用这个来进行输出。

错误

Go 程序使用 error 值来表示错误状态。

与 fmt.Stringer 类似, error 类型是一个内建接口:

1
2
3
type error interface {
Error() string
}

(与 fmt.Stringer 类似,fmt 包在输出时也会试图匹配 error。)

通常函数会返回一个 error 值,调用的它的代码应当判断这个错误是否等于 nil, 来进行错误处理。

1
2
3
4
5
6
i, err := strconv.Atoi("42")
if err != nil {
fmt.Printf("couldn't convert number: %v\n", err)
return
}
fmt.Println("Converted integer:", i)

error 为 nil 时表示成功;非 nil 的 error 表示错误。

示例:

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
package main

import (
"fmt"
"math"
)

type ErrNegativeSqrt float64

func (e ErrNegativeSqrt) Error() string{
if e < 0 {
return fmt.Sprintf("cannot Sqrt negative number:%v",float64(e))
}
return ""
}

func Sqrt(x float64) (float64, error) {
i := ErrNegativeSqrt(x)
if i < 0 {
return -1, i
}else{
return math.Sqrt(x),nil
}
return 0, nil
}

func main() {
fmt.Println(Sqrt(2))
fmt.Println(Sqrt(-2))
}

Readers

io 包指定了 io.Reader 接口, 它表示从数据流结尾读取。

Go 标准库包含了这个接口的许多实现, 包括文件、网络连接、压缩、加密等等。

io.Reader 接口有一个 Read 方法:

1
func (T) Read(b []byte) (n int, err error)

Read 用数据填充指定的字节 slice,并且返回填充的字节数和错误信息。 在遇到数据流结尾时,返回 io.EOF 错误。

例子代码创建了一个 strings.Reader。 并且以每次 8 字节的速度读取它的输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
"fmt"
"io"
"strings"
)

func main() {
r := strings.NewReader("Hello, Reader!")
b := make([]byte, 8)
for {
n, err := r.Read(b)
fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
fmt.Printf("b[:n] = %q\n", b[:n])
if err == io.EOF {
break
}
}
}
  • 本文作者: 风月
  • 本文链接: /golang-grammar-01/
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!