Golang基础整理


基础

每个程序都是由包构成的,程序从main包开始运行,规定包名与导入路径中最后一个元素相对应import math/rand包中的源码均以package rand开始。

在Go中如果名字是大写开头就是可以导出的,以小写字母开头是不可以导出的(这种可不可以导出是相对于包外而言的),强行导出不可导出的元素编译器会报错。

函数

Go的返回值可以被命名,会被视作定义在函数顶部的变量,返回值的名称一改具有一定意义,用于文档当中,没有参数的return的返回已经命名的返回值,但是这种做法仅在短函数中使用,长函数会影响代码的可读性。

package main

import "fmt"

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return
}

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

变量

var 语句声明一个变量列表, var语句可以出现在包级别和函数级别中

package main

import "fmt"

var c, python , java bool

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

短变量声明:=可以在类型明确的时候替代var, 但是函数外必须使用var字段。

Go中的基本类型

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // uint8 的别名

rune // int32 的别名
    // 表示一个 Unicode 码点

float32 float64

complex64 complex128

与导入包一样,变量声明同样支持分组:

var (
    ToBe   bool       = false
    MaxInt uint64     = 1<<64 - 1
    z      complex128 = cmplx.Sqrt(-5 + 12i)
)

int, uint, uintptr在32位系统上是32位的在64为操作系统上就是64位的

const Pi = 3.14 // 常量的定义,不能使用:=

循环

基本的for循环由三部分做成,初始化语句,条件表达式,后置语句,其中条件表达式是唯一必须的,可以去掉分号相当于while

package main

import "fmt"

func main() {
    sum := 1
    for sum < 1000 {
        sum += sum
    }
    fmt.Println(sum)
}

如果省略条件表达式那么for{}相当于while True:

条件语句

if可以使用简短语句先赋值后判断:

if x := 2; x > 1 {  // X 属于局部变量,仅在if语句内有效

} else {

}

switch语句:

    switch os := runtime.GOOS; os {  // switch也可以没有参数
    case "darwin":  // 取值无须常量
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
        fallthrough // 语句会继续执行
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.\n", os)
    }

defer语句会将函数推迟到外层函数返回后执行,被推迟的函数的参数会立刻求值(会被保存起来),但函数本身不会被调用。推迟的函数调用会被压如栈中,按照先进后出的顺序进行调用。

指针

var p *int零值是nil, 其取值赋值与C语言类似

结构体

一个结构体是一个字段

package main

import "fmt"

type Vertex struct {
    X int
    Y int
}

func main() {
    v := Vertex{1, 2}  // 初始化方法
    v.X = 4 // 访问方法

    p := &v 
    p.x = 5  // 结构体字段可以通过结构体字段来访问,(*p).x 很麻烦,语言允许使用隐式的间接引用!
}
var (
    v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
    v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
    v3 = Vertex{}      // X:0 Y:0
    p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

数组

var a [10]int, 数组大小是数组定义的一部分,数组大小是不可以改变的。

p := [5]int{1, 2, 3, 4, 5} // 初始化的一种方法

切片

数组的大小是固定的,而切片则为数组元素可以提供动态大小和灵活的使用方法,[]T表示T类型的切片。

a[low:high] 与Python类似这是一个前闭后开区间,其零值是nil。

package main

func main() {
    primes := [6]int{2, 3, 5, 7, 11, 13}
    var s []int = primes[1:4]
}

切片就像数组的引用,切片并不存储任何数据,仅描述了底层数组的一段,更改切片元素会更改底层元素的值,共享同意数组的切片值也会因此改变。

package main

import "fmt"

func main() {
    names := [4]string{
        "John",
        "Paul",
        "George",
        "Ringo",
    }
    fmt.Println(names)

    a := names[0:2]
    b := names[1:3]
    fmt.Println(a, b)

    b[0] = "XXX"
    fmt.Println(a, b)
    fmt.Println(names)
}
/*
[John Paul George Ringo]
[John Paul] [Paul George]
[John XXX] [XXX George]
[John XXX George Ringo]
*/

[]bool{true, true, false}语句会先创建一个[3]bool{true, true, false}长度为3的数组,然后构建一个引用它的切片。

切片有两个属性,长度和容量可以通过lencap来获取,

package main

import "fmt"

func main() {
    s := []int{2, 3, 5, 7, 11, 13}
    printSlice(s) // len=6 cap=6 [2 3 5 7 11 13]

    // 截取切片使其长度为 0
    s = s[:0] 
    printSlice(s) // len=0 cap=6 []

    // 拓展其长度
    s = s[:4] 
    printSlice(s)  //len=4 cap=6 [2 3 5 7]

    // 舍弃前两个值
    s = s[2:]
    printSlice(s) //len=2 cap=4 [5 7]
}

func printSlice(s []int) {
    fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

我们可以把cap理解成原cap减去左指针走过的值。

切片可以使用make来创建,这也是构建动态数组的一种方法make([]int, NUM_LEN, NUM_CAP),切片可以包含任意值(切片包含切片当然可以)。

package main

import (
    "fmt"
    "strings"
)

func main() {
    // 创建一个井字板(经典游戏)
    board := [][]string{
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
        []string{"_", "_", "_"},
    }

    // 两个玩家轮流打上 X 和 O
    board[0][0] = "X"
    board[2][2] = "O"
    board[1][2] = "X"
    board[1][0] = "O"
    board[0][2] = "X"

    for i := 0; i < len(board); i++ {
        fmt.Printf("%s\n", strings.Join(board[i], " "))
    }
}

使用append()函数向切片中追加元素,s = append(s, 1, 2, 3) 可以添加任意长的数据。

Range

package main

import "fmt"

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)
    }
}

for i, _ := range pow
for _, value := range pow // 忽略的方法

for i := range pow  //直接忽略第二个值

Map

map像python中的字典,其零值为nil,nil映射,如果使用var m map[string]int创建,那么这就是个nil你不能添加键值对,make可以对map进行初始化。

map直接初始化时必须要带键名:

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m = map[string]Vertex{
    "Bell Labs": Vertex{   // 可以省略写成{40.68433, -74.39967}
        40.68433, -74.39967,
    },
    "Google": Vertex{  // 可以省略写成{37.42202, -122.08408}
        37.42202, -122.08408, 
    },
}

func main() {
    fmt.Println(m)
}

插入或修改m[key] = elem, 获取elem = m[key], 删除delete(m, key), 检测elem, ok = m[key]

函数值

函数也是一种值,可以像其他值一样传递

hypot := func(x, y float64) float64 {
        return math.Sqrt(x*x + y*y)
    }

函数闭包

Go的函数可以是一个闭包,闭包是一个函数值,它引用了其函数体外的变量,这个函数可以访问和修改这个值,换句话说,这个函数和这个值绑定在一起了。

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}
/*
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
21 -42
28 -56
36 -72
45 -90
*/

方法和接口

方法

Go中没有类,可以使用结构体类型来定义方法,方法是一类带特殊的接受者参数的函数:

type Vertex struct {
    X, Y struct
}

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

接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法, 接受者可以使指针,但是不能是类似*int这样的,前面说过了,内建的类型不行, 如果你不使用*T那么就是副本操作,并没有系该原变量。

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK  与上面一样,编译器会解释为(&p).Scale(10)

带指针参数的函数必须接受一个指针,而以指针为接收者的方法被调用时,接收者既能为值又能为指针

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK 这种情况下,方法调用 p.Abs() 会被解释为 (*p).Abs()。

接受者类型应该为指针。

接口

接口类型十一组方法签名定义的集合,接口类型的变量可以保存任何实现了这些方法的值。

package main

import (
    "fmt"
    "math"
)

type Abser interface {
    Abs() float64
}

type MyFloat float64

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

type Vertex struct {
    X, Y float64
}

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


func main() {
    var a Abser
    f := MyFloat(-math.Sqrt2)
    v := Vertex{3, 4}

    a = f  // a MyFloat 实现了 Abser
    a = &v // a *Vertex 实现了 Abser

    // 下面一行,v 是一个 Vertex(而不是 *Vertex)
    // 所以没有实现 Abser。
    a = v

    fmt.Println(a.Abs())
}

类型通过实现一个接口的所有方法来实现该接口,不需要显式声明,这样接口可以实现在任何包中,无须在每一个接口上实现上新增接口的名称。

接口也是值(value, type),可以作为函数参数和返回值, 接口值保存了一个具体底层类型的具体值,接口调用方法会调用底层类型的同名方法。

底层值为nil仍可以调用接口中的方法, 而nil接口值不保存接口值也保存类型。

空接口

空接口可以保存任何类型的值(因为每个类型都实现了至少零个方法)


package main

import "fmt"

func main() {
    var i interface{}
    describe(i) // (<nil>, <nil>)

    i = 42
    describe(i) // (42, int)

    i = "hello"
    describe(i) // (hello, string)
}

func describe(i interface{}) {
    fmt.Printf("(%v, %T)\n", i, i)
}

类型断言

类型断言提供了访问接口值底层具体值的方法t := i.(T),如果i中保存的不是T那么将会引起一个panic,使用f, ok := i.(T)则不会引起panic,f会被赋值为T的零值, ok为false, 使用type关键字:

package main

import "fmt"

func do(i interface{}) {
    switch v := i.(type) {
    case int:
        fmt.Printf("Twice %v is %v\n", v, v*2)
    case string:
        fmt.Printf("%q is %v bytes long\n", v, len(v))
    default:
        fmt.Printf("I don't know about type %T!\n", v)
    }
}

func main() {
    do(21)  // Twice 21 is 42
    do("hello") // "hello" is 5 bytes long
    do(true) // I don't know about type bool! 默认 v与接口类型相同。
}

Stringer

fmt 包中定义的 Stringer 是最普遍的接口之一

type Stringer interface {
    String() string
}

package main

import "fmt"

type Person struct {
    Name string
    Age  int
}

func (p Person) String() string {  // 这里不能是引用类型,如果下面是引用类型就可以用*Person
    return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
    a := Person{"Arthur Dent", 42}
    z := Person{"Zaphod Beeblebrox", 9001}
    fmt.Println(a, z) // Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
}

并发

goroutine是由Go运行时管理的轻量级线程,goroutine共享内存地址空间,因此在访问共享内存时必须进行同步

信道

信道带有类型,与映射和切片一样使用前必须创建ch := make(chan int), 发送和接收操作在对方准备好之前都会阻塞,这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- 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)
}

带缓冲的信道

ch := make(chan int, 100)仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    ch <- 3   // deadlock
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

range 和 close

循环 for i := range c 会不断从信道接收值,直到它被关闭。发送者且仅可能是发送者才能用close关闭信道,若没有值可以接收且信道已被关闭,那么在执行完v, ok := <-ch, ok为false,向一个已经关闭的信道发送数据会引发panic,信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

select 语句

select语句让一个Go程等待多个通信操作,select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {  // for 和 select对应
        select {
        case c <- x:  //准备好了就执行, 没准备好就看下一个准没准备好
            x, y = y, x+y
        case <-quit:  // 多个分支都准备好了就随机选择一个运行
            fmt.Println("quit")
            return  // 退出select
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)

}

select 中的其它分支都没有准备好时,default 分支就会执行,为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:

select {
case i := <-c:
    // 使用 i
default:
    // 从 c 中接收会阻塞时执行
}

sync.Mutex

如果我们不需要线程间进行通信呢?我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突,我们使用互斥锁来解决这个问题, sync.Mutex提供两种方法LockUnlock

package main

import (
    "fmt"
    "sync"
    "time"
)

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
    v   map[string]int
    mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    c.v[key]++
    c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
    c.mux.Lock()
    // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    defer c.mux.Unlock()
    return c.v[key]
}

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i < 1000; i++ {
        go c.Inc("somekey")
    }

    time.Sleep(time.Second)
    fmt.Println(c.Value("somekey"))
}

文章作者: Hanjun Liu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Hanjun Liu !
 上一篇
Flask模板 Flask模板
在视图函数中会向客户端返回一行HTML代码,但是一个完整的HTML需要很多行代码,如果采用return的形式是非常不方便维护的。 在动态的web程序中,返回的HTML数据要根据相应的变量做出动态生成,这时我们就需要模板引擎(template
2020-02-06
下一篇 
2020.2.5 LeetCode刷题记录 2020.2.5 LeetCode刷题记录
617. 合并二叉树class TreeNode: def __init__(self, x): self.val = x self.left = None self.right =
2020-02-05
  目录