Sycamore

Phoenix reborns from the ashe

0%

GO 语言基础

本教程仅简要记录GO语言与已学过的PythonC++Swift等语言的基础方法上面的差别.

并不做深入讨论,仅作为备忘录存在。

变量定义

1
2
3
4
var a int
var a int = 1
var a = 1
a := 1

四种变量的定义方式:

  • 第一种不初始化,此时必须指明变量的类型
  • 第二种初始化,但是可能初始化值有歧义,需要显式定义数据类型
  • 第三种和第四种等价,建议第四种,编译器会自行判断变量类型,类似Python

这里注意:a := 1只能在函数中这么使用

全局变量的声明方式为:

1
2
3
4
5
var(
a int
b float32
c string
)

空白标识符在函数返回的时候使用,表示不获取该位置的返回值

1
2
3
4
5
6
7
8
9
func main() {
_,numb,strs := numbers() //只获取函数返回值的后两个
fmt.Println(numb,strs)
}
//一个可以返回多个值的函数
func numbers()(int,int,string){
a , b , c := 1 , 2 , "str"
return a,b,c
}

变量不进行声明,会给一个默认值:

数据类型 初始化默认值
int 0
float32 0
pointer nil
string ""

常量定义

1
2
const a int = 1
const a = 1

全新的用法:iota:

iota是在声明常量的时候自动加一的编译器存储的变量。

1
2
3
4
5
const (
a = iota
b = iota
c = iota
)
1
a = 0, b = 1, c = 2

第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1

在使用枚举声明常量,如果变量不声明初始值,其初始值定义方式与上一行一样,有贯穿效果的。但是iota的值是会不断+1的!!

1
2
3
4
5
6
7
8
9
10
11
12
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
)
//0 1 2 ha ha 100 100 7 8

条件语句

条件语句——switch

1
2
3
4
5
6
7
8
switch var1 {
case val1:
...
case val2:
...
default:
...
}

Go中的Switch是不带贯穿的,如果默认,在完成case后会自动break。如果我们需要执行后面的 case,可以使用 fallthrough

但是Go中可以同时判断多个case:

1
case val1, val2, val3

此外,其还可以对type进行判断。 ^_^

1
2
3
4
5
6
7
8
9
switch x.(type){
case type:
statement(s);
case type:
statement(s);
/* 你可以定义任意个数的case */
default: /* 可选 */
statement(s);
}

条件语句——select

  • select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
  • select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
  • 如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
select {
  case <- channel1:
    // 执行的代码
  case value := <- channel2:
    // 执行的代码
  case channel3 <- value:
    // 执行的代码

    // 你可以定义任意数量的 case

  default:
    // 所有通道都没有准备好,执行的代码
}

以下描述了 select 语句的语法:

  • 每个 case 都必须是一个通道

  • 所有 channel 表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果任意某个通道可以进行,它就执行,其他被忽略。

  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。

    否则:

    1. 如果有 default 子句,则执行该语句。
    2. 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。

函数

1
2
3
4
func function_name( a int, b string ) (int, string, float32) {
函数体
return a, b, c
}

如果传入的参数一样,在最后定义类型即可:func swap(x, y string){}

引用传参

C++一样,Go是带有指针的!!!!QAQ

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

在调用的时候,需要传入参数的地址:swap(&a, &b)

函数作为实参

1
2
3
4
5
6
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}

/* 使用函数 */
fmt.Println(getSquareRoot(9))

这个和Swift有点像,如果一个参数的数据需要计算出来,就可以通过这种方式定义一个函数,来对实参的初始化数据进行计算。

闭包 T_T

Swift的闭包基本一样,属于一种匿名的函数,通常用于在函数内部定义函数,或者作为参数传递。

1
2
3
4
5
6
7
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}

↑ 该函数名称为getSequence,没有传入参数,返回的是一个类型为:返回值为int的函数。

当该函数被初始化到一个实参上,该函数内部数据i被创建并保留。此时这个实参就为一个func() int的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func getSequence() func() int {
i := 0
return func() int {
i += 1
return i
}
}

func main(){
/* nextNumber 为一个函数,函数 i 为 0 */
nextNumber := getSequence()

/* 调用 nextNumber 函数,i 变量自增 1 并返回 */
fmt.Println(nextNumber())
fmt.Println(nextNumber())
fmt.Println(nextNumber())

/* 创建新的函数 nextNumber1,并查看结果 */
nextNumber1 := getSequence()
fmt.Println(nextNumber1())
fmt.Println(nextNumber1())
}
// 1 2 3 1 2

↑,作用就是i被存储到了函数内部,每次重新函数创建都会重建内部参数。

方法

类中或者结构体中的函数就是方法

其定义语句为:

1
2
3
func (variable_name variable_data_type) function_name() [return_type]{
/* 函数体*/
}
1
2
3
4
5
6
7
8
9
10
/* 定义结构体 */
type Circle struct {
radius float64
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}

数组

声明以及初始化

Go 语言数组声明需要指定元素类型元素个数,语法格式如下:

1
var balance [10]float32

数组的初始化:

1
2
var numbers = [5]int{1, 2, 3, 4, 5}
numbers := [5]int{1, 2, 3, 4, 5}

:=的用法和变量时候的用法一致。

如果数组的长度不是固定的!可以用...进行替代

1
2
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

Go语言允许仅对某些位置的元素进行初始化

1
balance := [5]float32{1:2.0,3:7.0}

这样,只有索引为1和3的元素进行了初始化,其余元素均为0。

二维数组

1
2
// var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type
var threedim [5][10][4]int

初始化

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

注意:以上代码中倒数第二行的 } 必须要有逗号,因为最后一行的 } 不能单独一行,也可以写成这样

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

对二维数组的访问和其余语言一样。

向函数传递数组

1
2
3
4
5
6
7
func myFunction(param [10]int) {
....
}// 设定数组长度

func myFunction(param []int) {
....
}// 未设定数组长度

指针 T_T

C++基本一样的指针结构

声明方式:

1
2
3
// var var_name *var-type
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */

可以认为在指针操作中:

&运算符是获得地址的操作

*运算符是对对地址进行一个解析的操作,来获得该地址上存储的数据。

空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil。

nil 指针也称为空指针。

nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。

一个指针变量通常缩写为 ptr。

指针数组

指针数组为,一个数组,其中的每一个元素都是一个指针。初始化方式:

1
var ptr [MAX]*int;

指针的数组可以通过遍历的方式对应数组的每一个元素。

1
2
3
4
5
6
7
8
9
func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int;

for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
}

指向指针的指针

1
var ptr **int

在解析指向指针的指针,也需要用**来解析。

指针作为函数参数

这个和c++一样。其实就是一个引用传参的变体。将地址传进去,这样对该地址的数据的操作,函数外侧该数据也会发生变化。

结构体

Swift的结构体大差不差,值传递的类,定义如下:

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

声明如下:

1
2
variable_name := structure_variable_type {value1, value2...valuen}
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

注意,和Swift一样,结构体是值传递,也就是说,对于a=b,是新建一个一样的结构体赋值给a,所以对a的更改,并不会影响b

所以对于函数的传参,值传参是无法更改函数外结构体的内容,必须用引用传参或者指针传参。

对切片的声明

1
var identifier []type

↑采用未声明长度的数组进行切片的声明。

↓或者采用make()函数进行创建

1
2
var slice1 []type = make([]type, len)
slice1 := make([]type, len)

也可以指定容量的大小,capacity作为可选参数,定义切片的最大长度。

1
make([]T, length, capacity)

切片初始化

1
2
3
4
5
s :=[] int {1,2,3 } // []中是空的
s := arr[:] //数组arr的引用
s := arr[startIndex:] // 从startIndex到末尾
s := arr[:endIndex] // 从开头到endIndex
s1 := s[startIndex:endIndex] // 两者中间的切片

切片拥有len()cap()函数,分别用来获得长度以及测量切片最长多长。

如果是空的切片,则和nil相比较。

切片是可以通过类似Python中的[:]进行截取的。

切片拥有append()copy()函数,前者用来增加元素,后者用来拷贝切片到新的切片。

我们可以看出切片,实际的是获取数组的某一部分,len切片<=cap切片<=len数组,切片由三部分组成:指向底层数组的指针、len、cap。

Range

Python一样,用来和for一起遍历数组,但是其和python中加入了enumrate一样,是可以获得Index的。

1
2
3
4
5
6
for key, value := range oldMap {
newMap[key] = value
}
for _, value := range oldMap {
a = value
}

Map

Map 是一种无序的键值对的集合。

Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。

其实就是Python中的dict

1
2
/* 使用 make 函数 */
map_variable := make(map[KeyType]ValueType, initialCapacity)

initialCapacity 是可选的参数,用于指定 Map 的初始容量。Map 的容量是指 Map 中可以保存的键值对的数量,当 Map 中的键值对数量达到容量时,Map 会自动扩容。如果不指定 initialCapacity,Go 语言会根据实际情况选择一个合适的值。

接口!! Interface!!

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

import "fmt"

type Shape interface {
    area() float64
}

type Rectangle struct {
    width  float64
    height float64
}

func (r Rectangle) area() float64 {
    return r.width * r.height
}

type Circle struct {
    radius float64
}

func (c Circle) area() float64 {
    return 3.14 * c.radius * c.radius
}

func main() {
    var s Shape
    s = Rectangle{width: 10, height: 5}
fmt.Printf("矩形面积: %f\n", s.area())// 同样的area,不同的方法,在func()更改
    s = Circle{radius: 3}
    fmt.Printf("圆形面积: %f\n", s.area())
}

错误处理

Go内置的错误接口类型:

1
2
3
type error interface {
Error() string
}
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
48
49
package main

import (
    "fmt"
)

// 定义一个 DivideError 结构
type DivideError struct {
    dividee int
    divider int
}

// 实现 `error` 接口
func (de *DivideError) Error() string {
    strFormat := `
    Cannot proceed, the divider is zero.
    dividee: %d
    divider: 0
`
    return fmt.Sprintf(strFormat, de.dividee)
}

// 定义 `int` 类型除法运算的函数
func Divide(varDividee int, varDivider int) (result int, errorMsg string) {
    if varDivider == 0 {
            dData := DivideError{
                    dividee: varDividee,
                    divider: varDivider,
            }
            errorMsg = dData.Error()
            return
    } else {
            return varDividee / varDivider, ""
    }

}

func main() {

    // 正常情况
    if result, errorMsg := Divide(100, 10); errorMsg == "" {
            fmt.Println("100/10 = ", result)
    }
    // 当除数为零的时候会返回错误信息
    if _, errorMsg := Divide(100, 0); errorMsg != "" {
            fmt.Println("errorMsg is: ", errorMsg)
    }

}

接口与错误处理要和结构体合在一起。

并发!!!

1
go f(x, y, z)
1
2
3
4
5
6
7
8
9
10
11
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}

func main() {
go say("world")
say("hello")
}

通道Channel

通道(channel)是用来传递数据的一个数据结构。

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

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

对通道进行声明:

1
ch := make(chan int)

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

通道缓冲区

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

1
ch := make(chan int, 100)

带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。