半天时间 Go 语言的基本实践

上班两个月,写了 C++,写了 Pathon,最近又要开始写 Go,唯独没有用我最擅长的 Java。。。

Go 开发环境的安装

下载地址:https://golang.org/dl/,选择对应的操作系统。
例如 Mac 系统选择 go1.12.darwin-amd64.pkg,安装完成后添加到环境变量:

export PATH=$PATH:/usr/local/go/bin

通过执行 go version 来确定安装是否完成:

go version

Go HelloWorld

创建 go_hello_world.go

package main

import "fmt"

func main() {
   /* Go HelloWorld */
   fmt.Println("Hello, World!")
}

通过执行 go run go_hello_world.go 来运行:

go run go_hello_world.go

分析上面的代码:

  • package main包声明,指明这个文件属于哪个包。package main 表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。
  • import "fmt"引入包声明。告诉 Go 编译器这个程序需要使用 fmt 包,fmt 包实现了格式化 IO(输入/输出)的函数。
  • func main() {函数main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)。
  • 可以使用 /.../ 或者 /*...*/ 来定义注释。
  • 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Println,那么使用这种形式的标识符的对象就可以被外部包的代码所使用,这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected )。
  • 需要注意的是 { 不能单独放在一行。

Go 基础语法

Go 标记
Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。

行分隔符
在 Go 程序中,一行代表一个语句结束。
每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾。

标识符
标识符用来命名变量、类型、函数名、结构字段等程序实体。
一个标识符实际上就是一个或是多个字母、数字、下划线组成的序列,但是第一个字符必须是字母或下划线而不能是数字。

关键字
具体参见:https://golang.org/ref/spec#Keywords

Go 数据类型

  • 布尔型
    • true false
  • 数字类型
    • uint8 uint16 uint32 uint64
    • int8 int16 int32 int64
    • float32 float64 complex64 complex128
    • byte rune uint int uintptr
  • 字符串类型
    • Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。。
  • 派生类型
    • 指针类型(Pointer)
    • 数组类型
    • 结构化类型(struct)
    • Channel 类型
    • 函数类型
    • 切片类型
    • 接口类型(interface)
    • Map 类型

Go 变量

声明变量的一般形式是使用 var 关键字。
第一种,指定变量类型,声明后若不赋值,使用默认值。

var i int
i = 1

第二种,根据值自行判定变量类型。

var i = 1

第三种,省略 var,使用 :=将由编译器自动推断类型,只能在函数体中出现。

i := 1

值类型和引用类型:

  • 所有像 intfloatboolstring 这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值。例如:
func main() {
    var i = 1
    var j = i
    fmt.Println(&i) // 取地址
    fmt.Println(&i, " != ", &j) // 地址不同
}
  • 一个引用类型的变量 r1 存储的是 r1 的值所在的内存地址(数字),或内存地址中第一个字所在的位置。例如:

并行赋值:
例如:

func main() {
    var a, b int
    var c string
    a, b, c = 1, 2, "abc"
    // 或者 a, b, c := 1, 2, "abc"
    fmt.Println(a, b, c);
}
  • 如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a,两个变量的类型必须是相同
  • 并行赋值的一个作用:比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到:val, err = Func1(...)
  • 空白标识符 _ 也被用于抛弃值,如值 1:_, i = 1, 2 中被抛弃。
    • _ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。
    • 例如我们可能不需要使用函数范围的错误 err,那么可以使用 val, _ = Func1(...)

Go 常量

定义格式:
const identifier [type] = value。可以省略类型 [type],因为编译器可以根据变量的值来推断其类型。

func main() {
    const i int = 1
    const j = "abc"
    
    fmt.Println(i, j);
}

Go 运算符

跟 Java 差不多,具体参见:

func main() {
    var i int = 1
    var p *int
    
    p = &i
    
    fmt.Println(i, *p, p); // 1 1 0xc210000018
}

Go 条件语句

跟 Java 差不多。

func main() {
    var i int = 1
    
    if i > 0 { // 注意不要用括号 if (i > 0)
        fmt.Println("i > 0")
    } else { // 注意 else 不能换行
        fmt.Println("i <= 0")
    }
    
    var j int = 2
    switch j { // 注意不要用括号 switch (j)
        case 1:
            fmt.Println("one")
        case 2:
            fmt.Println("two")
        case 3:
            fmt.Println("three")
    }
}

Go 循环语句

跟 Java 差不多。

func main() {
    for n := 0; n <= 5; n++ {  // 注意不要用括号 for (n := 0; n <= 5; n++)
        if n%2 == 0 {
            continue
        }
        fmt.Println(n)
    }
}

Go 函数

Go 语言最少有个 main() 函数。
函数定义:

func function_name( [parameter list] ) [return_types] {
   函数体
}

示例:

// 一个返回值
func f1(i int, j int) int {
    return i + j
}

// 多个返回值
func f2(i int, j int) (int, int) {
    return i + j, i - j
}

func main() {
    var m = f1(1, 2)
    fmt.Println(m) // 3
    
    var p, q = f2(1, 2)
    fmt.Println(p, q) //3 -1
}

Go 变量作用域

变量可以在三个地方声明:

  • 函数内定义的变量称为局部变量

    • 它们的作用域只在函数体内,参数和返回值变量也是局部变量。
  • 函数外定义的变量称为全局变量

    • 全局变量可以在任何函数中使用。
    • 全局变量可以在整个包甚至外部包(被导出后)使用。
    • Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。
var g = 1
func main() {
    fmt.Println(g)
}
  • 函数定义中的变量称为形式参数
    • 形式参数会作为函数的局部变量来使用。

不同类型的局部和全局变量默认值为:

  • int - 0
  • float32 - 0
  • pointer - nil

Go 数组

数组定义:

var variable_name [SIZE] variable_type

示例:

func main() {
    var a [5] int
    fmt.Println(a) // [0 0 0 0 0]
    
    a[4] = 100
    fmt.Println(a[4]) // 100
    fmt.Println(a) // [0 0 0 0 100]
    
    b := [5] int {1, 2, 3, 4, 5}
    fmt.Println(b) // [1 2 3 4 5]
    
    var twoD [2][3] int
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            twoD[i][j] = i + j
        }
    }
    
    fmt.Println(twoD) // [[0 1 2] [1 2 3]]
}

Go 指针

指针定义:

var var_name *var-type

示例:

func zeroval(ival int) {
    ival = 0
}

func zeroptr(iptr *int) {
    *iptr = 0
}

func main() {
    i := 1
    fmt.Println("initial:", i) // 1
    
    zeroval(i)
    fmt.Println("zeroval:", i) // 1
    
    zeroptr(&i)
    fmt.Println("zeroptr:", i) // 0
    
    fmt.Println("pointer:", &i) // 0xc210000018
}

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

Go 结构体

定义结构体:

type struct_variable_type struct {
   member definition;
   member definition;
   ...
   member definition;
}
  • 使用结构体进行变量的声明和定义:
variable_name := structure_variable_type {value1, value2...valuen}
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
  • 如果要访问结构体成员,需要使用点号 . 操作符
  • 可以定义指向结构体的指针

示例:

type person struct {
    name string
    age  int
}
func main() {
    fmt.Println(person{"Bob", 20}) // {Bob 20}
    
    fmt.Println(person{name: "Alice", age: 30}) // {Alice 30}
    
    fmt.Println(person{name: "Fred"}) // {Fred 0}
    
    fmt.Println(&person{name: "Ann", age: 40}) // &{Ann 40}
    
    s := person{name: "Sean", age: 50}
    fmt.Println(s.name) // Sean
    
    sp := &s
    fmt.Println(sp.age) // 50
    
    sp.age = 51
    fmt.Println(sp.age) // 51
}

Go 切片(Slice)

Go 语言切片是对数组的抽象。
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用。
Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

可以声明一个未指定大小的数组来定义切片:

var identifier [] type

使用 make() 函数来创建指定 初始长度 len 的切片:

var slice1 [] type = make([]type, len)

slice2 := make([]type, len)

var slice3 [] type = make([]type, len, capacity) // capacity 为容量,为可选参数

切片初始化:

func main() {
    // 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3。其 cap=len=3
    s1 :=[] int {1,2,3} 
    fmt.Println(s1) // [1 2 3]
    
    // 定义一个数组
    arr := [10] int {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    // 初始化切片,是数组 arr 的引用
    s2 := arr[:]
    fmt.Println(s2) // [1 2 3 4 5 6 7 8 9 10]
    
    // 将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片
    s3 := arr[1 : 5]
    fmt.Println(s3) // [2 3 4 5]
    
    // 缺省 endIndex 时将表示一直到 arr 的最后一个元素
    s4 := arr[1 : ]
    fmt.Println(s4) // [2 3 4 5 6 7 8 9 10]
    
    // 缺省 startIndex 时将表示从 arr 的第一个元素开始
    s5 := arr[ : 5]
    fmt.Println(s5) // [1 2 3 4 5]
}
  • 切片是可索引的,并且可以由 len() 方法获取长度。
  • 切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
  • 一个切片在未初始化之前默认为 nil,长度为 0
  • 如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来,使用 append()copy() 方法。示例:
func main() {
    var numbers [] int
    fmt.Println(numbers) // []
    
    numbers = append(numbers, 1)
    fmt.Println(numbers) // [1]
    
    numbers = append(numbers, 2, 3, 4)
    fmt.Println(numbers) // [1 2 3 4]
    
    // 创建切片 numbers1 是之前切片的两倍容量
    numbers1 := make([]int, len(numbers), (cap(numbers))*2)
    copy(numbers1, numbers)
    fmt.Println(numbers1) // [1 2 3 4]
}

Go 范围(Range)

Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对的 key 值。
示例:

func main() {
    // 定义一个数组
    nums := [] int{2, 3, 4}
    sum := 0
    
    // 使用 range 将传入 index 和值两个变量
    for i, num := range nums {
        fmt.Println("index = ", i, "value = ", num)
    }
    
    // 求和,使用空白标识符"_"来忽略索引
    for _, num := range nums {
        sum += num
    }
    fmt.Println("sum:", sum) // 9
    
    // 用在 map 的键值对上。
    kvs := map[string]string{"a": "apple", "b": "banana"}
    for k, v := range kvs {
        fmt.Printf("%s -> %s\n", k, v)
    }
    
    // 用来枚举 Unicode 字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
    for i, c := range "go" {
        fmt.Println(i, c)
    }
}

Go Map(集合)

Map 的定义:

// 声明变量,默认 map 是 nil
var map_variable map[key_data_type]value_data_type

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

示例:

func main() {
    m := make(map[string]int)
    
    m["k1"] = 7
    m["k2"] = 13
    
    fmt.Println("map:", m) // map[k1:7 k2:13]
    
    v1 := m["k1"]
    fmt.Println("v1: ", v1) // v1:  7
    
    fmt.Println("len:", len(m)) // len: 2
    
    delete(m, "k2")
    fmt.Println("map:", m) // map: map[k1:7]
    
    _, prs := m["k2"]
    fmt.Println("prs:", prs) // prs: false
}

Go 类型转换

类型转换:type_name(expression)
示例:

func main() {
    var sum int = 17
    var count int = 5
    var mean float32
    mean = float32(sum)/float32(count)
    fmt.Printf("mean 的值为: %f\n", mean) // 3.400000
}

Go 接口

接口的定义:

type interface_name interface {
   method_name1 [return_type]
   method_name2 [return_type]
   method_name3 [return_type]
   ...
   method_namen [return_type]
}

示例:

import "fmt"
import "math"

type geometry interface {
    area() float64
    perim() float64
}

type rect struct {
    width, height float64
}
type circle struct {
    radius float64
}

func (r rect) area() float64 {
    return r.width * r.height
}
func (r rect) perim() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}
func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

func main() {
    r := rect{width: 3, height: 4}
    c := circle{radius: 5}
    
    measure(r)
    measure(c)
}

Go 错误处理

通过内置的错误接口 error,定义:

type error interface {
    Error() string
}

我们可以在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个错误信息。
示例:

import "fmt"
import "errors"

func f1(arg int) (int, error) {
    if arg == 42 {
        return -1, errors.New("can't work with 42")
    }
    return arg + 3, nil
}

type argError struct {
    arg  int
    prob string
}

func (e *argError) Error() string {
    return fmt.Sprintf("%d - %s", e.arg, e.prob)
}

func f2(arg int) (int, error) {
    if arg == 42 {
        return -1, &argError{arg, "can't work with it"}
    }
    return arg + 3, nil
}

func main() {
    for _, i := range []int{7, 42} {
        r, e := f1(i)
        if e != nil {
            fmt.Println("f1 failed:", e)
        } else {
            fmt.Println("f1 worked:", r)
        }
    }
    
    for _, i := range []int{7, 42} {
        r, e := f2(i)
        if e != nil {
            fmt.Println("f2 failed:", e)
        } else {
            fmt.Println("f2 worked:", r)
        }
    }
}

Go 并发

通过 go 关键字来开启 goroutine
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 定义:

go 函数名( 参数列表 )

示例:

import "fmt"
import "time"

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world") // go 语句开启一个新的运行期线程, 即 goroutine
    say("hello")
}

输出:

hello
world
hello
world
hello
world
hello
world
hello

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

// 把 v 发送到通道 ch
ch <- v

// 从 ch 接收数据,并把值赋给 v
v := <- ch

示例:

func f(messages chan string) {
    messages <- "ping"
}
func main() {
    // 使用 chan 关键字创建通道
    messages := make(chan string)
    
    // go 语句开启一个新的运行期线程, 即 goroutine
    go f(messages)
    
    // 将 messages 传到主线程
    msg := <- messages
    fmt.Println(msg) // ping
}

更多关于通道的示例:

字符串格式化

参考:Go by Example: String Formatting

package main

import "fmt"
import "os"

type point struct {
    x, y int
}

func main() {
    p := point{1, 2}
    fmt.Printf("%v\n", p) // {1 2}

    // The `%+v` variant will include the struct's field names. 打印字段名
    fmt.Printf("%+v\n", p) // {x:1 y:2}

    // The `%#v` variant prints a Go syntax representation of the value.
    fmt.Printf("%#v\n", p) // main.point{x:1, y:2}

    // To print the type of a value, use `%T`. 打印类型
    fmt.Printf("%T\n", p) // main.point

    // Formatting booleans is straight-forward.
    fmt.Printf("%t\n", true) // true

    // Use `%d` for standard, base-10 formatting. 打印十进制
    fmt.Printf("%d\n", 123) // 123

    // This prints a binary representation. 打印二进制
    fmt.Printf("%b\n", 14) // 1110

    // `%x` provides hex encoding. 打印十六进制
    fmt.Printf("%x\n", 456) // 1c8

    // This prints the character corresponding to the given integer. (ASCII Table)
    fmt.Printf("%c\n", 33) // !

    // 打印浮点数
    fmt.Printf("%f\n", 78.9) // 78.900000

    // 科学计数法
    fmt.Printf("%e\n", 123400000.0) // 1.234000e+08
    fmt.Printf("%E\n", 123400000.0) // 1.234000E+08

    // 打印字符串
    fmt.Printf("%s\n", "\"string\"") // "string"

    // To double-quote strings as in Go source, use `%q`. 自动双引号包裹
    fmt.Printf("%q\n", "\"string\"") // "\"string\""

    // `%x` renders the string in base-16, with two output characters per byte of input.
    fmt.Printf("%x\n", "hex this") // 6865782074686973

    // To print a representation of a pointer, use `%p`. 打印指针
    fmt.Printf("%p\n", &p) // 0x414020

    // 指定数字宽度,默认右对齐
    fmt.Printf("|%6d|%6d|\n", 12, 345) // |    12|   345|

    // 指定数字小数点位数
    fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45) // |  1.20|  3.45|

    // 指定左对齐
    fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45) // |1.20  |3.45  |

    // 指定字符串宽度
    fmt.Printf("|%6s|%6s|\n", "foo", "b") // |   foo|     b|

    // 指定左对齐
    fmt.Printf("|%-6s|%-6s|\n", "foo", "b") // |foo   |b     |

    // Sprintf 格式化字符串并返回,但是不会打印到 os.Stdout
    s := fmt.Sprintf("a %s", "string")
    fmt.Println(s) // a string

    // 使用 fmt.Fprintf 打印到 io.Writers,而不是 os.Stdout
    fmt.Fprintf(os.Stderr, "an %s\n", "error") // an error
}

参考:
Go 语言教程
Go by Example

推荐阅读更多精彩内容