GO-Start.md

初始入门可以参考菜鸟教程 ,整体比较系统,也是入门参考的文档,涉及的相关字面内容,在此不进行赘述。仅记录一些,相比python、perl而言的个性化特点。

基础语法记录

用的比较多,教程中也没有展开介绍,列在这,后续有需要的情况下,酌情补充。

key_word 示例 说明
break
default
func
interface
select
case
defer
go
map
struct
chan
else
goto
package
switch
const
fallthrough
if
range
type
continue
for
import
return
var

变量声明

  • 变量声明需要明确声明,这在perl、python中可能没有那么严格, 声明支持 var x typesx := "" 两种方式
  • 常量有单独的声明, const identifier [type] = value ,用来声明不会进行改变的值(只支持布尔型、数字型和字符串型)。在使用常量时候,会生成一个自增长的特殊变量iota会对未赋值的常量进行自动复制。这部分在使用中需要注意。

运算符

常规运算符,这里不进行展开了,其中赋值运算符遇到两个之前没有遇到过的, <<=>>= ,左移后复制,和右移后赋值。之前在其他语言没见过的。其实就是将对应的整数型数值转换成 2进制的值,然后对二进制的数据进行移动后得到一个新的二进制数据,然后解析成整数。

运算符 描述 实例
<<= 左移后赋值 C <<= 2 等于 C = C << 2
>>= 右移后赋值 C >>= 2 等于 C = C >> 2

条件语句

在 GO中,不支持三目运算符。所以不能使用类似python中的 maxNum = a if a > b else b

函数

哎,为啥所有编程语言的函数声明格式不能一致点呢,哪怕关键字统一统一也行呀。有 subdef 在go里面的关键字是func
Go语言的函数定义示例如下:

1
2
3
4
5
6
7
8
9
10
func function_name( [parameter list] ) [return_types] {
函数体
}
/*
func:函数由 func 开始声明
function_name:函数名称,参数列表和返回值类型构成了函数签名。
parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
函数体:函数定义的代码集合。
*/

相比于其他的语言,比较特殊的是需要声明返回值的数据类型。如果函数有返回值,则该参数是必须的!如果返回多个值,则需要声明每个值的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 声明局部变量 */
var result int

if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}

// 返回多个结果值时,函数声明的格式
func swap(x, y string) (string, string) {
return y, x
}

在go语言中,同时支持值传递(传递数值,变更不会影响函数外的变量)和引用传递(传递地址,变更会影响函数外的变量)。

作用域

这部分和其他编程语言基本一致。

数组

这部分比较特殊的,就是在声明数组类型的时候,可以指定元素个数

1
2
3
4
5
6
7
8
9
10
11
12
//格式
var arrayName [size]dataType

//示例
var balance [10]float32

//初始化
var numbers = [5]int{1, 2, 3, 4, 5}
numbers := [5]int{1, 2, 3, 4, 5}

// 将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}

指针

Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。在指针类型前面加上 * 号(前缀)来获取指针所指向的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var var_name *var-type
// var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。以下是有效的指针声明:
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */

// 获取指针,并通过指针访问值的示例
func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}

// 当一个指针被定义后没有分配到任何变量时,为空指针,它的值为 nil
if(ptr != nil) /* ptr 不是空指针 */
if(ptr == nil) /* ptr 是空指针 */

结构体

可以简单的理解成其他语言中的对象/类。结构体在定义完成后,就属于一种可以用于声明的变量。对应的结构体也可以作为函数的传入参数。

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
type Books struct {
title string
author string
subject string
book_id int
}

func main() {
//几种不同的赋值方式:
// 创建一个新的结构体
var Book1 = Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407}
// 也可以使用 key => value 格式
var Book2 = Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407}
fmt.Println(Book2)
// 忽略的字段为 0 或 空
var Book3 = Books{title: "Go 语言", author: "www.runoob.com"}
fmt.Println(Book3)
// 声明 Book1 为 Books 类型,并单独赋值
var Book4 Books
Book4.title = "Go 语言"
Book4.author = "www.runoob.com"
Book4.subject = "Go 语言教程"
Book4.book_id = 6495407

// 访问结构体的内容,使用 "结构体.成员名"
fmt.Printf( "Book 4 title : %s\n", Book1.title)
fmt.Printf( "Book 4 author : %s\n", Book1.author)
fmt.Printf( "Book 4 subject : %s\n", Book1.subject)
fmt.Printf( "Book 4 book_id : %d\n", Book1.book_id)
}

切片

切片和python的数组概念类似,go语言的数组更像是python中的元组。

切片的一些方法

方法 示例 说明
len() len(x) 获取切片的长度。
cap() cap(x) 可以测量切片最长可以达到多少
append() x = append(x, 1) 切片中追加一个元素
copy() copy(x,x1) 拷贝一个切片

集合

和python的字典,perl的hash类似。
不过不通过的是,Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 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
/* 使用 make 函数定义一个集合 */
map_variable := make(map[KeyType]ValueType, initialCapacity)
/*
KeyType 是键的类型,
ValueType 是值的类型,
initialCapacity 是可选的参数,用于指定 Map 的初始容量。Map 的容量是指 Map 中可以保存的键值对的数量,当 Map 中的键值对数量达到容量时,Map 会自动扩容。
*/

// 创建并赋值,使用字面量创建 Map
m := map[string]int{
"apple": 1,
"banana": 2,
"orange": 3,
}

// 获取键值对
v1 := m["apple"]
v2, ok := m["pear"] // 如果键不存在,ok 的值为 false,v2 的值为该类型的零值

// 遍历 Map
for k, v := range m {
fmt.Printf("key=%s, value=%d\n", k, v)
}

// 删除键值对
delete(m, "banana")

遍历

语言范围,看下来其实相当于其他编程语言中的遍历,包括遍历数组,字典等结构。遍历返回值均为2个,第一项为索引,第二项为值。

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
map1 := make(map[int]float32)
map1[1] = 1.0
map1[2] = 2.0
map1[3] = 3.0
map1[4] = 4.0

// 遍历map类型的数据
// 读取 key 和 value
for key, value := range map1 {
fmt.Printf("key is: %d - value is: %f\n", key, value)
}
// 读取 key
for key := range map1 {
fmt.Printf("key is: %d\n", key)
}
// 读取 value
for _, value := range map1 {
fmt.Printf("value is: %f\n", value)

// 遍历数组
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}

递归

Go 语言支持递归。但我们在使用递归时,需要设置退出条件,否则递归将陷入无限循环中。
尤其注意的是,go是支持函数的递归的(函数中调用函数自身),这在其他的语言中其实很少见,会存在先有鸡还是先有蛋的逻辑悖论。
递归的一个示例如下:

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
//通过 Go 语言的递归函数实例阶乘
func Factorial(n uint64)(result uint64) {
if (n > 0) {
result = n * Factorial(n-1)
return result
}
return 1
}

func main() {
var i int = 15
fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i)))
}

// 通过 Go 语言使用递归方法实现求平方根的代码:
func sqrtRecursive(x, guess, prevGuess, epsilon float64) float64 {
if diff := guess*guess - x; diff < epsilon && -diff < epsilon {
return guess
}

newGuess := (guess + x/guess) / 2
if newGuess == prevGuess {
return guess
}

return sqrtRecursive(x, newGuess, guess, epsilon)
}

func sqrt(x float64) float64 {
return sqrtRecursive(x, 1.0, 0.0, 1e-9)
}

func main() {
x := 25.0
result := sqrt(x)
fmt.Printf("%.2f 的平方根为 %.6f\n", x, result)
}

前面的阶乘等方法可能本身不能凸显递归本身的优势,但是从求指定精度的平方根来看,递归确实有其独特的数学应用基础。

语言接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
看下来,和 python中对象的方法比较类似,不是一个独立的函数,而是依托于对应的数据类型的方法。

go并发

这可能是go之所以能脱颖而出的重要优势。Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
在使用go进行多线程分析的时候,可以使用直接在对应的函数前添加go即可。(go中的多线程是无序的,所以go中的程序执行顺序必须是不影响业务逻辑的)

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

import (
"fmt"
"time"
)

func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}

Goroutine的规则:
当新的Goroutine开始时,Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束。当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码。
main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行。

go 通道

通道(channel)是用来传递数据的一个数据结构。通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
go语言中,除了直接共享内存,还可以在多个goriutine之间完成数据通信,实现数据共享。

1
2
3
4
5
6
7
8
// 创建通道 chan关键字,后面是缓冲区的数据类型
ch := make(chan int)
// 创建带缓冲区的通道
ch := make(chan int, 100)

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

注意: 默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。
注意: 如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 // 从通道 c 中接收

        fmt.Println(x, y, x+y)
}

遍历和关闭通道

Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:v, ok := <-ch, 不过为了区分通道是在等待还是已经终止存储,需要在通道存储数据结束后,对通道进行关闭 close(ch)
通道除了作为数据的通信,还可以用来确保对应的子线程执行完成(利用通道的阻塞功能)。

错误处理

-------------本文结束感谢您的阅读-------------