阅读(3646) (0)

Go 基础

2016-02-24 15:28:39 更新

这小节我们将要介绍如何定义变量、常量、Go内置类型以及Go程序设计中的一些技巧。

定义变量

Go语言里面定义变量有多种方式。

使用var关键字是Go最基本的定义变量方式,与C语言不同的是Go把变量类型放在变量名后面:

//定义一个名称为“variableName”,类型为"type"的变量
var variableName type

定义多个变量

//定义三个类型都是“type”的变量
var vname1, vname2, vname3 type

定义变量并初始化值

//初始化“variableName”的变量为“value”值,类型是“type”
var variableName type = value

同时初始化多个变量

/*
    定义三个类型都是"type"的变量,并且分别初始化为相应的值
    vname1为v1,vname2为v2,vname3为v3
*/
var vname1, vname2, vname3 type= v1, v2, v3

你是不是觉得上面这样的定义有点繁琐?没关系,因为Go语言的设计者也发现了,有一种写法可以让它变得简单一点。我们可以直接忽略类型声明,那么上面的代码变成这样了:

/*
    定义三个变量,它们分别初始化为相应的值
    vname1为v1,vname2为v2,vname3为v3
    然后Go会根据其相应值的类型来帮你初始化它们
*/
var vname1, vname2, vname3 = v1, v2, v3

你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化:

/*
    定义三个变量,它们分别初始化为相应的值
    vname1为v1,vname2为v2,vname3为v3
    编译器会根据初始化的值自动推导出相应的类型
*/
vname1, vname2, vname3 := v1, v2, v3

现在是不是看上去非常简洁了?:=这个符号直接取代了vartype,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用var方式来定义全局变量。

_(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值35赋予b,并同时丢弃34

_, b := 34, 35

Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了i但未使用。

package main

func main() {
    var i int
}

常量

所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。

它的语法如下:

const constantName = value
//如果需要,也可以明确指定常量的类型:
const Pi float32 = 3.1415926

下面是一些常量声明的例子:

const Pi = 3.1415926
const i = 10000
const MaxThread = 10
const prefix = "astaxie_"

赋值后打印出这些常量后的结果为

1594777056(1)

Go 常量和一般程序语言不同的是,可以指定相当多的小数位数(例如200位), 若指定給float32自动缩短为32bit,指定给float64自动缩短为64bit,详情参考链接

内置基础类型

Boolean

在Go中,布尔值的类型为bool,值是truefalse,默认为false

//示例代码
var isActive bool  // 全局变量声明
var enabled, disabled = true, false  // 忽略类型的声明
func test() {
    var available bool  // 一般声明
    valid := false      // 简短声明
    available = true    // 赋值操作
}

数值类型

整数类型有无符号和带符号两种。Go同时支持intuint,这两种类型的长度相同,但具体长度取决于不同编译器的实现。Go里面也有直接定义好位数的类型:runeint8int16int32int64byteuint8uint16uint32,uint64。其中runeint32的别称,byteuint8的别称。

需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。

如下的代码会产生错误:invalid operation: a + b (mismatched types int8 and int32)

var a int8

var b int32

c:=a + b

另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。

浮点数的类型有float32float64两种(没有float类型),默认是float64

这就是全部吗?No!Go还支持复数。它的默认类型是complex128(64位实数+64位虚数)。如果需要小一些的,也有complex64(32位实数+32位虚数)。复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位。下面是一个使用复数的例子:

var c complex64 = 5+5i
//output: (5+5i)
fmt.Printf("Value is: %v", c)

字符串

我们在上一节中讲过,Go中的字符串都是采用UTF-8字符集编码。字符串是用一对双引号("")或反引号(`)括起来定义,它的类型是string

//示例代码
var frenchHello string  // 声明变量为字符串的一般方法
var emptyString string = ""  // 声明了一个字符串变量,初始化为空字符串
func test() {
    no, yes, maybe := "no", "yes", "maybe"  // 简短声明,同时声明多个变量
    japaneseHello := "Konichiwa"  // 同上
    frenchHello = "Bonjour"  // 常规赋值
}
        

在Go中字符串是不可变的,例如下面的代码编译时会报错:cannot assign to s[0]

var s string = "hello"
s[0] = 'c'

但如果真的想要修改怎么办呢?下面的代码可以实现:

s := "hello"
c := []byte(s)  // 将字符串 s 转换为 []byte 类型
c[0] = 'c'
s2 := string(c)  // 再转换回 string 类型
fmt.Printf("%s\n", s2)

Go中可以使用+操作符来连接两个字符串:

s := "hello,"
m := " world"
a := s + m
fmt.Printf("%s\n", a)

修改字符串也可写为:

s := "hello"
s = "c" + s[1:] // 字符串虽不能更改,但可进行切片操作
fmt.Printf("%s\n", s)

如果要声明一个多行的字符串怎么办?可以通过' '来声明:

m := `hello
    world`

``` 括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。例如本例中会输出:

hello
    world

错误类型

Go内置有一个error类型,专门用来处理错误信息,Go的package里面还专门有一个包errors来处理错误:

err := errors.New("emit macho dwarf: elf header corrupted")
if err != nil {
    fmt.Print(err)
}

Go数据底层的存储

下面这张图来源于Russ Cox Blog中一篇介绍Go数据结构的文章,大家可以看到这些基础类型底层都是分配了一块内存,然后存储了相应的值。


map

map也就是Python中字典的概念,它的格式为map[keyType]valueType

我们看下面的代码,map的读取和设置也类似slice一样,通过key来操作,只是sliceindex只能是`int`类型,而map多了很多类型,可以是int,可以是string及所有完全定义了==!=操作的类型。

// 声明一个key是字符串,值为int的字典,这种方式的声明需要在使用之前使用make初始化
    numbers := make(map[string]int) // 另一种map的声明方式     var numbers map[string]int     numbers["one"] = 1  //赋值

    numbers["tow"] = 2 //赋值

    numbers["three"] = 3 //赋值

    fmt.Println("第一个数字是: ", numbers["one"]) // 读取数据

    fmt.Println("第二个数字是: ", numbers["tow"]) // 读取数据

    fmt.Println("第三个数字是: ", numbers["three"]) // 读取数据

按照上面的输入,输出结果为


这个map就像我们平常看到的表格一样,左边列是key,右边列是值

使用map过程中需要注意的几点:

  • map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取
  • map的长度是不固定的,也就是和slice一样,也是一种引用类型
  • 内置的len函数同样适用于map,返回map拥有的key的数量
  • map的值可以很方便的修改,通过numbers["one"]=11可以很容易的把key为one的字典值改为11
  • map和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制

map的初始化可以通过key:val的方式初始化值,同时map内置有判断是否存在key的方式

通过delete删除map的元素:

// 初始化一个字典
rating := map[string]float32{"C":5, "Go":4.5, "Python":4.5, "C++":2 }
// map有两个返回值,第二个返回值,如果不存在key,那么ok为false,如果存在ok为true
csharpRating, ok := rating["C#"]
if ok {
    fmt.Println("C# is in the map and its rating is ", csharpRating)
} else {
    fmt.Println("We have no rating associated with C# in the map")
}
delete(rating, "C")  // 删除key为C的元素

上面说过了,map也是一种引用类型,如果两个map同时指向一个底层,那么一个改变,另一个也相应的改变:

m := make(map[string]string)
m["Hello"] = "Bonjour"
m1 := m
m1["Hello"] = "Salut"  // 现在m["hello"]的值已经是Salut了

make、new操作

make用于内建类型(mapslice 和channel)的内存分配。new用于各种类型的内存分配。

内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:

new返回指针。

内建函数make(T, args)new(T)有着不同的功能,make只能创建slicemapchannel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slicenil。对于slicemapchannel来说,make初始化了内部的数据结构,填充适当的值。

make返回初始化后的(非零)值。

下面这个图详细的解释了newmake之间的区别。