菜鸟教程

语言教程

计算机软件经历了数十年的发展,形成了多种学术流派,有面向过程编程、面向对象编程、函数式编程、面向消息编程等,这些思想究竟孰优孰劣,众说纷纭。

除了 OOP 外,近年出现了一些小众的编程哲学,Go 语言对这些思想亦有所吸收。例如,Go 语言接受了函数式编程的一些想法,支持匿名函数与闭包。再如,Go语言接受了以 Erlang 语言为代表的面向消息编程思想,支持 goroutine 和通道,并推荐使用消息而不是共享内存来进行并发编程。总体来说,Go 语言是一个非常现代化的语言,精小但非常强大。

Go 语言最主要的特性:

  • 自动垃圾回收
  • 更丰富的内置类型
  • 函数多返回值
  • 错误处理
  • 匿名函数和闭包
  • 类型和接口
  • 并发编程
  • 反射
  • 语言交互性

语法基础

仅仅列举一些值得注意的语法。

数据类型

使用 fmt.Sprintf 格式化字符串并赋值给新串:

1
fmt.Sprintf("Code=%d&endDate=%s", 123, "2020-12-31");

Go 中按类别大约有以下几种数据类型:

  • ”布尔型“、“数字型”、“字符串型”、“派生类型”;
  • 其中“派生类型”有以下几种:指针类型、数组类型、struct 类型、channel 类型、函数类型、切片类型、interface 类型、Map 类型;

变量

变量声明有以下几种方式:

1
2
3
4
5
6
7
8
// 一次声明多个变量
var identifier type

// 根据值自行判定变量类型。
var v_name = value

// 省略 var,使用 := 定义。左侧如果没有声明新的变量,就产生编译错误
v_name := value

声明多变量的方式:

1
2
3
4
5
6
7
var vname1, vname2, vname3 type
var (
    vname1 v_type1
    vname2 v_type2
)
var vname1, vname2, vname3 = v1, v2, v3
vname1, vname2, vname3 := v1, v2, v3 

指定变量类型,如果没有初始化,则变量默认为零值:

  • false0""nil

值类型、引用类型与 C++ 类似。

常量

常量定义关键词是 const,与变量类似,它的定义格式如下:

1
const identifier [type] = value

在 golang 常量中有一个特殊的语法 iota,下面是使用实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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)
}

条件语句

if 条件语句:

1
2
3
4
5
   /* 使用 if 语句判断布尔表达式 */
   if a < 20 {
       /* 如果条件为 true 则执行以下语句 */
       fmt.Printf("a 小于 20\n" )
   }

switch 条件语句(默认 break,使用 fallthrough 语句执行下一个 case):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
func main() {
   var x interface{}
     
   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("未知型")    
   }  
}

select 语句来自于 Linux 中的 select 这一 IO 模式(现在常用的是 epoll),它是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。

select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。

基本格式:

1
2
3
4
5
6
7
8
9
select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s);
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

循环

go 中只使用 for 这一个关键字进行循环,但是却可以执行 C 中所有关键字的功能:

1
2
3
4
5
6
7
8
9
for init; condition; post { }

for condition { }

for { }

for key, value := range oldMap {
    newMap[key] = value
}

其中 range 循环可以对字符串、数组、切片等进行迭代输出元素,下面举个例子:

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

func main() {
        strings := []string{"google", "runoob"}
        for i, s := range strings {
                fmt.Println(i, s)
        }

        numbers := [6]int{1, 2, 3, 5}
        for i,x:= range numbers {
                fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
        }  
}

在循环语句中可以使用 break/continue/goto 进行控制。

函数

Go 语言函数定义格式如下:

1
2
3
func function_name( [parameter list] ) [return_types] {
   // ...函数体
}

Go 语言的函数可以返回多个值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package main
import "fmt"

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

func main() {
   a, b := swap("Google", "Runoob")
   fmt.Println(a, b)
}

Go 语言可以定义引用传值:

1
2
3
4
5
6
func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保持 x 地址上的值 */
   *x = *y      /* 将 y 值赋给 x */
   *y = temp    /* 将 temp 值赋给 y */
}

数据类型

数组

声明数组语法:

1
2
3
var variable_name [SIZE] variable_type

var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type // 多维数组

初始化数组的方式:

1
2
3
4
5
6
7
8
9
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} // 自动推断数组长度

a := [3][4]int{  
 {0, 1, 2, 3} ,   /*  第一行索引为 0 */
 {4, 5, 6, 7} ,   /*  第二行索引为 1 */
 {8, 9, 10, 11},   /* 第三行索引为 2 */
}

指针

声明指针的语法:

1
var var_name *var-type

空指针的值为 nil

定义指针数组(存储着指针的数组)通过以下的方式:

1
var ptr [MAX]*int;

结构

与 C 语言中类似,声明的语法:

1
2
3
4
5
6
type struct_variable_type struct {
   member definition
   member definition
   ...
   member definition
}

切片

对数组的抽象,因为数组的长度是固定的,使用范围有限,与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

定义切片:

1
2
3
var identifier []type

var slice1 []type = make([]type, len, capacity) // capacity 是可选参数

范围

集合

Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。Map 的底层是通过哈希表实现的。

可以使用内建函数 make 也可以使用 map 关键字来定义 Map:

1
2
3
4
5
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)

接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/* 定义接口 */
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 语言的核心优势在于并发的实现,所以 go 在工程意义上是一个更适合做“分布式系统”的后端语言,而不适合做科学计算。

goroutine

我们可以用 go 关键词来开启一个 goroutine

  • goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
  • 同一个程序中的所有 goroutine 共享同一个地址空间。

语法格式如下:

1
go 函数名( 参数列表 )

channel

goroutine 是 go 语言中用于声明新的线程的方式,channel 则是 go 中用于进行线程间通信的方式。

  • 关键字 channel: 可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。
  • 声明 make:声明一个变量使用 make 函数。比如 make(chan int) 声明了一个,可以用于传递 int 类型数据的管道。
  • 操作符 <-: 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
  • 遍历通道,使用关键字 range:像遍历切片一样的 for 循环;
  • 关闭管道,使用函数 close:如果有某个 goroutine 正在进行 range 遍历,它只有在管道关闭时才会结束,否则会一直阻塞。

下面是一个声明并且使用的例子:

1
2
3
4
ch := make(chan int) // 指定第二个参数可以指定缓冲区大小

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据,并把值赋给 v

上面的 make 函数如果指定了第二个参数则指定了缓冲区大小。如果通道不带缓冲,在通道中有内容时,发送方会阻塞直到接收方从通道中接收了值。