叁叁肆

这个世界会好吗

453篇博客

A Bite of GoLang (1)

叁叁肆2018-09-19 11:02

本文来自网易云社区


作者:盛国存


0. 前言

A bite of GoLang(浅尝GoLang),本文只是Go语言的冰山一角,本文包含作者学习Go语言期间积累的一些小的经验,同时为了方便让读者了解到Go语言中的一些概念,文中包含了许多快速简洁的例子,读者后期可以去自行拓展。当然写这篇文章的灵感来源于GitHub上的 a bite of Python


1. 基础

1.0、环境搭建

1、下载安装包安装

通过浏览器访问下面的地址 https://golang.org/dl/ 要是自己的网络不能翻墙的话,可以访问下面的Go语言中文网 https://studygolang.com/dl 下载指定的版本的安装包直接下一步就可以安装完成;


2、命令行安装

Mac 利器 home brew 安装 go

brew update && brew upgrade
brew install git
brew install mercurial
brew install go

安装完成之后

vim ~/.bashrc
#GOROOT
export GOROOT=/usr/local/Cellar/go/1.7.4/libexec

#GOPATH
export GOPATH=$HOME/GoLangProject

#GOPATH bin
export PATH=$PATH:$GOPATH/bin

#GOPATH root bin
export PATH=$PATH:$GOROOT/bin
source ~/.bashrc

OK配合完成之后,输入go env验证一下是否配置成功

~ sheng$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOOS="darwin"
GOPATH="/Users/verton/GoLangProject"
GORACE=""
GOROOT="/usr/local/Cellar/go/1.7.4/libexec"
GOTOOLDIR="/usr/local/Cellar/go/1.7.4/libexec/pkg/tool/darwin_amd64"
CC="clang"
GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/z2/h48yrw8131g824_bvtw6584r0000gn/T/go-build415367881=/tmp/go-build -gno-record-gcc-switches -fno-common"
CXX="clang++"
CGO_ENABLED="1"


1.1、变量定义

1、通过var关键字

var a int
var b string

在Go语言中在定义变量的时候,是变量在前类型在后,现在你暂时先不用考虑那么多为什么,就先知道Go是这样的定义形式就可以了;当然可以多个变量一起定义,同时可以一起赋初值

var a,b,c bool
var m,n string = "Hello","World"
var (
    aa = 1
    bb = "hello world"
    cc = true
)

当然也可以让编译器自动决定类型,比如

var s,m,p,q = 1,"hahah",false,"xixiix"

2、使用 := 定义变量

s,m,p,q := 1,"hahah",false,"xixiix"

这样呢可以让代码写的更加简短一点,当然呢 := 只能在函数内使用,是不能在函数外使用的。(相关的函数的知识后面会做介绍)


1.2、内建变量类型

1、bool 、string

这两个类型就不做过多的介绍,因为基本每一门语言里面都有这两个类型,在Go语言里面也是一样的

2、(u)int、(u)int8、(u)int16、(u)int32、(u)int64、uintptr

上面这些就是Go的整数类型,加u和不加u的区别就是有无符号的区别,Go语言中的整数类型还分为两个大类,一个是规定长度的,比如:int8、int16、int32...,还有一种就是不规定长度的,它是根据操作系统来,在32位系统就是32位,在64位系统就是64位的,Go语言中没有int、long 这些类型,你想要定义一个相对较长的定义int64就可以了,最后uintptr就是Go语言的指针,后面我会再来介绍它

3、byte、rune

byte就不用过多介绍了,大家都知道字节类型,那rune是什么呢,这就是Go语言的“char”,因为char只有一个字节在使用中会有很多的坑,Go语言针对这点痛点做了一些优化

4、float32、float64、complex64、complex128

前面两个不过多介绍,浮点数类型32位和64位的,后面两个是一个复数的类型,complex64实部和虚部都是32位的,complex128实部和虚部都是64位的


1.3、常量与枚举

const a = 1
const b,c = 2,3
const (
    d = 5
    e,f = 6,7
)

常量数值可以作为各种类型使用,比如以下代码

var s,p = 3,4
m := math.Sqrt(s*s + p*p)
fmt.Println(m)

这段代码语法是编译不通过的,因为Sqrt的参数必须是一个浮点数类型;但是呢我们把是s、p定义成常量就可以编译通过了

const s,p = 3,4
m := math.Sqrt(s*s + p*p)
fmt.Println(m)

Go语言中的枚举类型就是通过const来实现,同时Go语言中还可以通过iota实现自增的功能

func enums(){

    const (
        a = iota
        b
        c
    )
    fmt.Println(a, b, c)
}

调用上面这个函数显而易见,会输出

0 1 2


1.4、条件语句

1、if

正常的条件判断我这边就不做过多的介绍,当然Go语言有它特别的地方,if的条件里可以赋值,比如:

举个读文件的例子,ioutil.ReadFile 这个方法有两个返回值,后面会详细的讲解,常规的写法是

const filename  = "file.txt"
content,err := ioutil.ReadFile(filename)
if err != nil {
    fmt.Println(err)
}else {
    fmt.Println(string(content))
}

Go语言可以整合成下面的写法

const filename  = "file.txt"
if content,err := ioutil.ReadFile(filename); err != nil {
    fmt.Println(err)
}else {
    fmt.Println(string(content))
}

2、switch

func eval(a int, b int, op string) int {

    var result int
    switch op {

        case "+":
            result = a + b
        case "-":
            result = a - b
        case "*":
            result = a * b
        case "/":
            result = a / b
        default:
            panic("unsupported op")
    }
    return result
}

看上面的这段代码,你发现和别的语言不一样的地方是怎么没有break,是的,Go语言中switch会自动break,除非使用fallthrough

同时,Go语言的switch还有另外一种写法,结合一个最常见的Switch用法举个例子吧,比如通过考试分数判断是否合格

func grade(score int) string {

    switch {
        case score > 100 || score < 0:
            panic("Wrong score")
        case score > 80:
            return "A"
        case score > 70:
            return "B"
        case score > 60:
            return "C"
        default:
            return "D"
    }
}

上面的一个写法可以发现switch后面是可以没有表达式的


1.5、循环

1、for

for关键字和其他语言有着共同的功能,同时还充当的Go语言中的 while 功能,Go语言中没有 while 关键字

for scanner.Scan() {
    fmt.Println(scanner.Text())
}

上面的循环代码省略了起始条件,省略了递增条件,就跟while的功能非常的类似

for {
    fmt.Println("hello world")
}

上面其实就是一个死循环,因为Go语言中经常会用到,后面的并发编程 Goroutine 的时候还会给大家继续介绍。


1.6、函数

1、普通函数

普通的函数定义我这边不再过多阐述,跟变量定义类似,函数名在前,函数返回类型在后

2、多返回值

这个是Go语言的不一样的地方,函数可以有多个返回值,比如 ioutil.ReadFile 这个函数就是有两个返回值,但是呢多返回值不要滥用,尽量贴合Go语言的风格,常规返回值和一个error,那我门这边可以将上面的加减乘除的例子做一下改造,因为panic之后程序就会终止了,我们可以将错误信息直接返回出来,让程序继续执行

func eval(a int, b int, op string) (int, error) {

    switch op {

        case "+":
            return a + b, nil
        case "-":
            return a - b, nil
        case "*":
            return a * b, nil
        case "/":
            return a / b, nil
        default:
            return 0, fmt.Errorf("unsupported op")
    }
}

3、函数可作为参数

func apply(op func(int, int) int, a, b int) int {
    return op(a, b)
}

Go语言定义这种函数在前,参数在后的复合函数非常的方便,只需要apply一个函数就可以了,当然在现实的过程中有时候也会了偷下懒,相关的op函数就直接写成一个匿名函数了

fmt.Println("sub(3, 4) is:", apply(
    func(a int, b int) int {
        return a - b
    }, 3, 4))

这样也是OK的

4、没有默认参数、没有可选参数

Go语言中没有其他语言类似Lambda这种很花哨的用法,除了一个可变参数列表

func sum(numbers ...int) int {
    s := 0
    for i := range numbers {
        s += numbers[i]
    }
    return s
}

上面就是一个参数求和函数


1.7、指针

1、指针不能运算

比如想对指针做加1运算,Go语言是不支持的;当然要是想在函数内部改变函数外面的变量的值,通过指针是如何实现的呢,如下图所示 

2、Go语言只有值传递

Go语言中想要改变变量的值,只能传一个指针进去,比如常见 a b 两个变量的值交换

func swap(a, b int) {
    *a, *b = *b, *a
}

当然呢,交换参数值是不建议上面的写法的


2. 内建容器

2.0、数组

1、定义

var arr1 [5]int
arr2 := [3]int{1, 3, 5}
arr3 := [...]int{2, 4, 6, 8, 10}
var grid [4][5]int

数组的定义和变量的定义类似,数组名在前类型在后; 常规的遍历操作也是类似

for i, v := range arr {
    fmt.Println(i, v)
}

i 是数组的下标,v是数组的值

2、数组是值类型

和上面值传递的概念类似,通过传参在函数内部是改变不了数组的值的;当然要是想改变相关的数组的值,可以通过指针来改变的。接下来的Slice可以直接解决上述的问题。


2.1、Slice(切片)的概念

1、Slice定义

Slice是什么呢?其实呢就是数组的一个View(视图),先来段代码热个身

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

fmt.Println("arr[2:6] =", arr[2:6])
fmt.Println("arr[:6] =", arr[:6])

结果输出:

arr[2:6] = [2 3 4 5]
arr[:6] = [0 1 2 3 4 5]

从上面的输出结果可以直接的看出,arr加一个下标区间都叫做Slice,Slice的区间是一个左闭右开的区间 当然我们还需要知道一个概念,Slice是没有数据的,是对底层Array的一个View,如何理解这个概念呢?简单的用一个例子来理解它

package main

import "fmt"

func updateSliceData(s []int) {
    s[0] = 666
}

func main() {
    arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}

    s1 := arr[2:]
    fmt.Println("s1 =", s1)
    s2 := arr[:]
    fmt.Println("s2 =", s2)

    fmt.Println("更新Slice数据 s1")
    updateSliceData(s1)
    fmt.Println(s1)
    fmt.Println(arr)

    fmt.Println("更新Slice数据 s2")
    updateSliceData(s2)
    fmt.Println(s2)
    fmt.Println(arr)
}

结果输出为:

s1 = [2 3 4 5 6 7]
s2 = [0 1 2 3 4 5 6 7]
更新Slice数据 s1
[666 3 4 5 6 7]
[0 1 666 3 4 5 6 7]
更新Slice数据 s2
[666 1 666 3 4 5 6 7]
[666 1 666 3 4 5 6 7]

2、ReSlice

就是在一个Slice上进一步slice,比如

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
ss := arr[:6]
ss = ss[:5]
ss = ss[2:]

结果输出:

[0 1 2 3 4]
[2 3 4]

3、Slice拓展

首先我们先看一个例子

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
s2 := s1[3:5]

大家或许会有疑问,这个s2不会报错么,要是不报错结果又是多少呢?

[2 3 4 5]
[5 6]

答案是可以,上述就是s1、s2的值,是不是跟你想的有点不一样。那么这又是为什么呢?

这就是为什么能把 6 这个值取出来的原因,因为slice是array的底层的一个view,是不是依然还是有点懵,具体又是如何实现的呢?


4、Slice实现

从上图是不是大体明白为什么上面那个例子能把6取出来了;看到这里大家也能大体明白Slice内部的ptr、len、cap是什么意思,ptr指向slice的开头的元素,len是slice的长度,cap代表底层的array从ptr开始到结束的长度,Slice是可以向后扩展的,但是不能向前扩展,所以只要不超过cap的长度slice都是可以扩展的,但是常规的s[i]取值是不可以超过len的。 用一个例子来简单的理解一下

arr := [...]int{0, 1, 2, 3, 4, 5, 6, 7}
s1 := arr[2:6]
fmt.Printf("len(s1): %d   ; cap(s1): %d ", len(s1), cap(s1))

输出结果:

len(s1): 4   ; cap(s1): 6


2.2、Slice(切片)的操作

1、向Slice添加元素

s3 := append(s2, 8)
s4 := append(s3, 9)
s5 := append(s4, 10)
fmt.Println("s3, s4, s5 =", s3, s4, s5)
fmt.Println("arr =", arr)

上面的这个例子打印出来结果又是多少呢?

s3, s4, s5 = [5 6 8] [5 6 8 9] [5 6 8 9 10]
arr = [0 1 2 3 4 5 6 8]

上面的9 ,10为什么不见了呢?因为Go语言在append数据超过cap长度的时候会分配一个更长的数组,如果arr不再使用的话就会被垃圾回收掉。 在append的过程中,由于是值传递的关系,len、cap都有可能会改变,所以呢必须要用一个新的slice来接收这个slice,通常会写成

s = append(s, value1)

2、创建slice

当然slice也可以直接通过var关键字创建

var s []int

这样创建的slice的初始值就是nil,别的语言中的null的意思,当然也是可以赋初值的,比如:

s1 := []int{2, 4, 6, 8}

就上面的Zero Value的Slice的情况,要是我这个时候对这个slice进行append操作会怎么样呢?这个slice的内部的len以及cap又是如何变化的呢?

var s []int
for i := 0; i < 100; i++ {
    fmt.Printf("%v, len = %d, cap = %d\n", s, len(s), cap(s))
    s = append(s, 2*i+1)
}

结果我就不输出了,因为相对太长,我把相应的结果总结一下,就是len就是一个步长为1由1增至100,cap呢?当系统发现不够存储的时候会分配一个现有长度两倍的空间。

当然在实际生产过程中,大多是使用的make关键字来创建slice的

s2 := make([]int, 4)
s3 := make([]int, 8, 16)

3、Copy Slice数据

func Copy(dst Writer, src Reader) (written int64, err error)

文档中可以看的很清晰,直接将第二个参数直接拷贝进第一个参数

s1 := []int{2, 4, 6, 8}
s2 := make([]int, 16)
copy(s2, s1)
fmt.Println(s2)

结果输出

[2 4 6 8 0 0 0 0 0 0 0 0 0 0 0 0]

4、Slice删除元素

s1 := []int{2, 4, 6, 8}
s2 := make([]int, 16)
copy(s2, s1)

比如我要删除 s2 中的第 3 个元素该如何操作呢?

s2 = append(s2[:2], s2[3:]...)

当然现实的使用中还会从slice中pop一个值出来,下面分别演示一下从s2头部pop和从s2尾部pop数据

front := s2[0]
s2 = s2[1:]
tail := s2[len(s2)-1]
s2 = s2[:len(s2)-1]


相关阅读:A Bite of GoLang (1)

A Bite of GoLang (2)

A Bite of GoLang (3)

A Bite of GoLang (4)

A Bite of GoLang (5)

A Bite of GoLang (6)

A Bite of GoLang (7)

A Bite of GoLang (8)


网易云免费体验馆0成本体验20+款云产品!

更多网易研发、产品、运营经验分享请访问网易云社区