Please enable Javascript to view the contents

Go初级: 语言特征

 ·  ☕ 13 分钟

系列

0. 官方教程

  • 离线教程(docker)
    1
    
      docker run -d -p 12345:9999 hunterhug/gotourzh
    
  • 官网教程
    GO 语言之旅

1. 数据类型

基本类型

布尔类型、数值类型(整型、浮点型、复数型)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 布尔类型
bool

// 字符串类型(一旦创建,内容不可变)
string

// 整型
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr
byte // uint8 的别名
rune // int32 的别名
     // 表示一个 Unicode 码点

// 浮点型
float32 float64

// 复数类型
complex64 complex128(复数类型, 具有 64 位的实部和 64 位的虚部, 总共占用 128 位的内存空间)

int 、 uint 和 uintptr 在 32 位系统上通常为 32 位宽, 在 64 位系统上则为 64 位宽

通常使用int作为整数类型, 如果需要使用固定大小, 则使用int8、int16、int32、int64; 如果需要无符号整数(比如位运算), 则使用unit、unint8、uint16、uint32、uint64;

uintptr类型: 专门用于存储指针的整数表示形式。在进行底层编程, 例如与 C 语言代码交互或者进行内存操作时, uintptr可以用来存储指针值。

复合类型

数组、切片、函数、指针、接口、映射、通道为复合类型,可由类型字面构造

array 数组

Go中array长度固定单一类型元素组成的编号序列。数组类型总是一维的,可通过组合构成多维的类型。

类型 [n]T 表示拥有n个T类型元素的数组

表达式为: var a [10]int

数组不能改变大小

数组通过a[n]读取相应索引的值, 0 < n取值范围 < len(a), 因此 Go 数组和切片都不支持反向索引(例如 a[-1])。

  • 数组声明

数组声明的语法参考变量声明,支持 var name [len]type 也支持 name := [len]type 短声明

  • 数组声明的同时,初始化数组
1
nums := [3]int{1, 2, 3}

还可以省略长度让编译器自动计算 nums := [...]{1, 2, 3}

如果指定索引赋值,中间的元素会置为0值 nums := [...]{11, 4:44, 55} 数组内容为 [11 0 0 44 55]

多维数组。array本身是一维的,通过组合类型实现多维数组。var twoD [2][3]int

slice 切片

切片 = 对数组连续段引用slice本身不存储数据。更改切片的元素会使底层数组的对应元素发生改变,并被其他共享此数组的slice观察到这些修改。

类型 []T 表示一个元素类型为T的切片

切片通过两个下标来界定,两者以冒号:分隔

sliceA = a[low:high], 此为半开区间,含首不含尾( 左闭右开 ). 例如 a[1:4] 包含 a[1] a[2] a[3]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
func main() {
  // 声明数组 声明+初始化
 	names := [4]string{ "John", "Paul", "George", "Ringo"}
	fmt.Println("Init: ", names)

  // 创建切片 切片 声明+初始化 a := []string{ "John", "Paul"}
	a := names[0:2]
	b := names[1:3]
	fmt.Println("Init-A: ", a, "Init-B:", b)

	b[0] = "XXX"

	fmt.Println("Change-A: ", a, "Init-B:", b)
	fmt.Println("Change: ", names)
}

输出为

1
2
3
4
Init:  [John Paul George Ringo]
Init-A:  [John Paul] Init-B: [Paul George]
Change-A:  [John XXX] Init-B: [XXX George]
Change:  [John XXX George Ringo]
  • 切片文法
    slice[low:high] 左闭右开 [low: high)

  • 切片省略写法(省略边界,取边界的默认值。上界默认值: 0; 下界默认值: len(slice))
    slice[:] == slice[:10] == slice[0:] == slice[0:10]

  • 切片长度、容量capacity

1
2
3
func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
  • nil切片
    零值为nil,长度容量皆为0,没有底层数组的切片 var s []int

  • make函数创建切片
    make函数可以创建一个元素为类型零值的数组,并返回引用切片。容量为可选参数

    创建len=5的int类型切片, a := make([]int, 5)

    创建len=0,cap=5的int类型切片,a := make([]int, 0, 5)

  • 切片的切片
    b := [][]string

  • 切片追加元素append
    new := append(a, 66)

  • 切片复制copy

    1
    2
    
    new := make([]string, len(s))
    copy(a, new)
    
  • 内置包slices功能
    slices.Equal(t, t2) 用来比较两个切片是否相同,相同返回 true

map 映射

键值对结构,使用前必须初始化,否则map是nil,无法往map中增加键值对,会报错。

  • 定义
 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
47
package main

import "fmt"

type selfDef struct {
  x,y int
}
func main() {
  // 映射文法
  var mapX = map[string]selfDef{
    "xx": selfDef{11, 21},
    "xxx": selfDef{
      111, 221,
    },
  }
  fmt.Println(mapX)

	// 映射文法, 若顶级类型只是一个类型名,则元素中可以省略类型名
	var mapX1 = map[string]selfDef{
		"xx":  {11, 21},
		"xxx": {111, 221},
	}
	fmt.Println(mapX1)

  var mapY map[string]selfDef
  mapY = make(map[string]selfDef)
  mapY["yy"] = selfDef{12, 22}
  fmt.Println(mapY)

  mapZ := make(map[string]selfDef)
  mapZ["zz"] = selfDef{13, 23}
  fmt.Println(mapZ)

  mapK := map[string]selfDef{
    "kk": selfDef{
      14,
      24,
    },
  }
  fmt.Println(mapK)

  type selfMap map[string]selfDef
  mapJ := selfMap{
    "jj": selfDef{15, 25},
  }
  fmt.Println(mapJ)
}
  • 映射修改

插入、修改元素 m[key] = elem
读取元素 elem = m[key]
删除元素 delete(m, key)
检查键存在(通过双赋值) elem, ok = m[key]
key存在: ok为true
key不存在: ok为false, elem为元素类型的零值
注意:当 elem k 未声明, 可以使用短变量声明elem, ok := m[key]

struct 结构体

struct 是一个字段的集合, 结构体字段是通过结构体指针进行访问,比如结构体的指针p, 可以通过 (*p).X访问字段X,可隐式解引用,简写为p.X访问

  • 声明( 定义 )
1
2
3
4
type selfDef struct {
  x int
  y int
}
  • 赋值( 初始化 )
1
2
3
4
5
6

func main(){
  a :=selfDef{1,2}
  // a :=selfDef{x: 1, y: 2} // 显式赋值 结构体文法
  fmt.Println(a)
}
  • 取值(读): 结构体字段通过点号 .访问
1
  fmt.Println(a.x)
  • 取值(通过struct指针访问)

结构体指针引用完整写法应为 (*p).x p是执行结构体的指针, go通过语法糖,可简写为 p.x

1
2
3
p := &a
p.x = 100
(*p).y = 11

零值

未明确初始值的变量声明, 均会被赋予该变量的零值

大类包含类型零值
布尔类型boolfalse
字符串类型string’’ (空字符串)
数值类型int* uint* float* 等0
复数类型complex64 complex128(0+0i) 实部为0, 虚部为0

类型转换

表达式T(v)可以将变量v转换为T类型

1
2
3
i := 42
f := float64(i)
u := uint(f)`

变量

变量的声明

关键字var 用来声明一个变量。结构为 var [名称] <类型> <=初始值>,类型在名称之后,类型、初始值可省略一个。

完整语法:var a int = 12
多变量声明:var a, b int = 1, 2
省略-值,声明零值变量:var a int
省略-类型,类型推断:var a = 1
短变量声明: a, b := true, 1 注意: 短变量声明只能在函数内使用
分组声明: 与import相同, var也可以使用括号组成一个语法块, 来声明一组变量。

函数内 := 可在类型明确的地方代替 var声明, 函数外的语句必须以关键字(var func import package 等等)开始, 因此 := 无法在函数外使用。

  • 举例: 普通变量声明
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package main

import "fmt"

var golang, python, java bool = true, false, false

func main() {

  // 初始值存在时, 可省略类型. 变量根据初始值获得类型
  var i, strVar, boolVar = 1, "str!", true

  fmt.Println(golang, python, java, i, strVar, boolVar)
}
  • 举例: 短变量声明
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func main() {
	var i, j int = 1, 2
	k := 3
	c, python, java := true, false, "no!"

	fmt.Println(i, j, k, c, python, java)
}
  • 举例:分组声明
1
2
3
4
var (
	ToBe   bool       = false
	MaxInt uint64     = 1<<64 - 1
)

变量作用域

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

import "fmt"

var golang, python, java bool

func main() {
  var i int
  fmt.Println(golang, python, java, i)
}

常量

常量类型有: 字符、字符串、布尔值、数值

语法: const [名称] <类型> = [值] 类型可省略,通过类型推导确定

常量不能使用语法糖:=

流程控制语句

条件、循环、分支和推迟语句来控制代码的流程。

循环: for

for是golang中唯一的循环结构

基本语法

基本的 for 循环由三部分组成,用分号隔开:

1
2
3
for <初始化语句: 0>; <条件表达式: 死循环>; <后置语句>{
    [每次迭代逻辑]
}
  • 初始化语句:在第一次迭代前执行(可省略)
  • 条件表达式:在每次迭代前求值
  • 后置语句: 在每次迭代的结尾执行

初始化语句通常为一句短变量声明(如 i:=0),该变量声明仅在 for 语句的作用域中可见

一旦条件表达式的布尔值为 false,循环迭代就会终止。 注意:和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面没有小括号( ),而大括号{ }是必须的。

举例

普通循环

循环10次 的不同写法

  1. 经典语法 initial;condition;after
1
2
3
for i:=0; i < 10; i++{
  fmt.Println(i)
}
  1. 可省略initial
1
2
3
4
i := 0
for i <10; i++{
  fmt.Println(i)
}

for 是 Go 中的「while」

go语言没有whiledo ... while语法,省略initial + after后,for 就是 Go 中的「while」。

1
2
3
4
5
i := 0
for i <10 {
  fmt.Println(i)
  i += 1
}

无限循环

for 后无condition即为死循环。再通过搭配 ifbreak 来实现跳出死循环。

1
2
3
4
5
6
7
8
initial 循环变量初始化
for {
  if condition 条件判断 {
    break
  }
  循环操作
  after 循环变量迭代
}

for-range 遍历

用来遍历切片、映射。遍历切片时,返回下标、对应元素的副本;遍历映射时,返回key、对应key的value;

1
2
3
4
5
6
7
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
}
  • 省略。通过_来省略下标;通过去掉value来省略值
    省略下标:
    1
    2
    3
    
    for _, value := range pow {
      	fmt.Printf("%d\n", value)
      }
    
    省略值:
    1
    2
    3
    
    for i := range pow {
      	pow[i] = 1 << uint(i) // == 2**i
      }
    

判断: if/else

Go 的 if 语句与 for 循环类似,条件表达式外无需小括号 ( ) ,但后面的大括号 { } 是必须的

基本用法

if condition {…} else {…}

1
2
3
4
5
if 7 % 2 == 0 {
  fmt.Println("7 是偶数")
} else {
  fmt.Println("7 是奇数")
}

带短变量声明的分支语句

if initial; condition {…}

同 for 一样, if 语句可以在条件表达式前执行一个简单的语句。

该语句声明的变量作用域仅在 if 之内。

1
2
3
4
if v := math.Pow(x, n); v < lim {
  return v
}
return lim

多分支判断

if condition {…} else if condition {…} else {…}

1
2
3
4
5
6
7
if n := 9; n < 0 {
  fmt.Println("n 是负数")
} else if n < 10 {
  fmt.Println("n 是一位数")
} else {
  fmt.Println("n 是多位数")
}

三元运算

Go 中没有三目运算。

分支: switch/case

switch的case语句从上到下顺序执行,直到匹配成功停止。

与其他语言的不同:1. 只运行匹配成功的第一个case(相当于在后面每个case中加了break),除非使用fallthrough语句结束,否则case会自动终止;2. case取值不仅限于整数、常量,可以为表达式等。

基本使用

case 后可以跟多个表达式,使用逗号分隔;建议设置 default 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
i := 1

switch i {
  case 1: 
    fmt.Println("i == 1")
  case 2, 3:
    fmt.Println("i 是 2 或 3")
  default:
    fmt.Println("i 不是 1 2 3")
}

表达式带短声明

1
2
3
4
5
6
switch i := 1; i{
  case i < 10:
    fmt.Println("i 小于10")
  default:
    fmt.Println("i 大于等于 10")
}

switch后省略表达式 == if/else

1
2
3
4
5
6
7
i := 1
switch {
  case i < 10:
    fmt.Println("i 小于10")
  default:
    fmt.Println("i 大于等于 10")
}

switch 判断类型

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func TypeIs(i interface{}) {
  switch t := i.(type) {
    case bool:
      fmt.Println("Type is bool")
    case int:
      fmt.Println("Type is int")
    default:
      fmt.Printf("Don't know type %T\n", t)
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.", os)
	}
}

也可以省略os变量的声明

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

func main(){
  fmt.Print("GO runs on")

  switch runtime.GOOS{
		case "linux":
		fmt.Println("Linux")
		case "darwin":
		fmt.Println("Darwin")
		default:
		fmt.Println("other")
	}
}

当省略条件时,等同于switch true{}(此格式,比一串if-else更清晰)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
func main(){
  t := time.Now()
  switch {
    case t.Hour() <12:
      fmt.Println("Morning! ")
    case t.Hour() < 17:
      fmt.Println("Afternoon! ")
    default:
      fmt.Println("Evening! ")
  }
}

等同于

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func main(){
  t := time.Now()
  if t.Hour() < 12 {
    fmt.Println("Morning! ")
  } else if t.Hour() < 17 {
    fmt.Println("Afternoon! ")
  } else {
    fmt.Println("Evening! ")
  }
}

逻辑运算符

相等 == 不等 != 大于> 大于等于>= 小于< 小于等于<=&&||

指数运算 e

3. 函数

函数定义

  • 基本语法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main

import "fmt"

func add(x int, y int) int {
  return x + y
}

func main() {
  fmt.Println(add(22, 33))
}
  • 其他语法

当连续的已命名形参类型相同时, 除最后一个形参的类型以外, 都可以省略。
x int, y int 可缩写为 x, y int

1
2
3
4
5
6
7
package main

import "fmt"

func add (x, y int) int {
  return x + y
}

函数返回值

  • 多返回值
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package main

import "fmt"

func swap(x, y string) (string, string) {
  return y, x
}

func main() {
  a, b := swap("hello", "world")
  fmt.Println(a, b)
}
  • 命名返回值

函数返回值可在函数顶部被定义, 此时return可以省略参数, 也就是直接返回( 长函数中不建议省略, 影响代码可读性)。

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

import "fmt"

func split(sum int) (x, y int){
  x = 7
  y = sum - x
  return
}

func main() {
  fmt.Println(split(17))
}

注意上面示例, 如果return y, x时, 返回值为10 7

方法

方法只是个带接收者参数的函数。

1
2
3
type Object struct {}

func (o Object) A() int {}

为非struct声明方法

1
2
type SelfInt int
func (s SelfInt) A() int {}

注意:接收者类型定义方法声明必须在同一包内。(因此无法为其他包中定义的类型声明方法,包括int之类的内置类型)

指针类型接收者

指针接收者,每个方法对值的修改都能传递,因此指针接收者比值接收者更常用。
值的类型为大型结构体时,指针接收者可以避免每次调用方法时复制值,更加高效。

接口

接口类型 的定义为一组方法签名

4. 包管理

导入

导入通过关键字import

基本语法

1
import "fmt"

导入多个包

1
2
import "fmt"
import "math"

导入多个包(推荐)

用括号组合了导入, 是分组形式的导入语句

1
2
3
4
import (
  "fmt"
  "math"
)

导出

在 Go 中, 如果一个名字以大写字母开头, 那么它就是已导出的。 例如, Pizza 就是个已导出名, Pi 也同样, 它导出自 math 包。

pizza 和 pi 并未以大写字母开头, 所以它们是未导出的。

在导入一个包时, 你只能引用其中已导出的名字。任何未导出的名字在该包外均无法访问。

其他语法特征

语法糖 - 简短变量声明:=

规则

  1. :=左侧必须要有新变量
  2. := 只能用于函数内部
  3. 多变量声明,可能会重新赋值
  4. 作用域

举例说明

  1. :=左侧必须要有新变量
    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    i := 1
    // := 左侧必须要有,至少一个新变量
    // 会报错no new variable on left side of :=
    i := 22
    
    // 以下场景也是相同道理
    func test(i int) {
        i := 111
    }
    
  2. 多变量声明,可能会重新赋值
    1
    2
    3
    
    field1, err := Func1()
    
    field2, err := Func2()
    
    如果存在新变量(field2),变量err会被重新赋值
    如果没有新变量,编译报错no new variable on left side of :=
  3. := 只能用于函数内部
    :=不能用来声明、初始化全局变量。函数外使用会编译报错syntax error: non-declaration statement outside function body
    (函数外的语句必须以关键字(var func import package 等等)开始, 因此 := 无法在函数外使用。)
  4. 作用域
    1
    2
    3
    4
    5
    6
    7
    
    func Test() {
       field, err:= Func1()         // 1号err
       if field == 1{
           field, err:= Func2()     // 2号err
           newField, err := Func3() // 3号err
       }
     }
    
    1号err 一个作用域,2号err3号err是相同作用域,因此 3号err 是对 2号err的重新赋值,但 1号err并不会因为后续的逻辑发生改变。

语法糖 - 可变参数...

可变参函数... 表示 0 或 多个参数。使用方法:在类型前加...

规则

  1. 可变参数必须位于参数列表的最后(否则编译时,会引起歧义);
  2. 可变参数在函数内作为切片来处理的;
  3. 函数调用时,可变参数可以不填(此时被当作切片的空值,即nil处理);
  4. 函数调用时,可变参数可以直接传入切片(函数内的操作,会影响传入切片的值)
  5. 可变参数类型必须相同,(不同类型,通过interface{}处理);

* & 指针

指针: 保存值的内存地址

操作符 * &

变量前面的 * 与 &是操作符,
&会生成一个指向其操作值的指针; (间接引用)
*表示指针指向的底层值;(重定向)

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

类型 *T

*T 表示指向T类型值的指针, 零值为nil

例如 var p *int

defer

defer语句 会将 函数返回 推迟到外层函数返回之后执行

defer调用的函数,其参数会立即求值压栈。但直到外层函数返回前,该函数都不会被调用。当外层函数返回时,defer函数会按后进先出的顺序调用返回。

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

import "fmt"

func main() {
  defer fmt.Println("done.")
  fmt.Println("Start: ")
  for i := 0; i < 3; i ++ {
    defer fmt.Println("Num", i)
  }
  defer fmt.Println("Print: NUM-2")
  fmt.Println("end")
  defer fmt.Println("Print: NUM-1")
}

输出为

1
2
3
4
5
6
7
8
Start: 
end
Print: NUM-2
Print: NUM-1
Num 2
Num 1
Num 0
done.

make 与 new 的区别

相同点:都是用于分配内存的内置函数。

new 用于分配内存并返回指向该内存的指针。主要用来创建一个引用类型的结构体,只有结构体可以用。
make 用于初始化内置的可变大小数据结构,并返回一个引用。(只用于 切片、映射和通道)

函数闭包

函数整体作为一个代码块,连带其引用额变量被压入栈,就形成了闭包

参考链接

中文资料

首先,安装Go环境。

然后,Go文档作为统一入口,包含

通过Go语言之旅,熟悉Go的语言特征;再通过Go 编程语言规范了解语言本身。

再通过实效Go编程 - 中文,了解Go代码的最佳实践。

想了解包相关,可以阅读

进一步探索 Go 的并发模型,参阅 Go高级并发模型 - 视频(幻灯片)以及深入 Go 并发模型(幻灯片)并阅读通过通信共享内存

函数 - Go语言的第一公民, 中又很多有趣的函数类型。

Go 博客有着众多关于 Go 的文章和信息。

Go 技术论坛有大量关于 Go 的中文文档和 Go 官方博客的翻译。

mikespook 的博客中有大量中文的关于 Go 的文章和翻译。

Go 内存模型

英文链接

GO 语言之旅 - 下一步

分享

Hex
作者
Hex
CloudNative Developer

目录