Files
build-web-application-with-…/2.2.md

430 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#2.2 GO基础
这小节我们将要介绍如何定义变量、常量GO内置类型以及一些GO设计中的技巧
##定义变量
GO语言里面定义变量有好几种方式。
最基本的定义变量如下go变量定义它的类型是跟在变量后面的而不是像C一样放前面
//定义一个名称为“variable_name”、类型为"type"的变量
var variable_name type
定义多个变量
//定义三个类型都是"type"的三个变量
var vname1,vname2,vname3 type
定义变量并且带有初始化的值
//初始化“variable_name”的变量为“value”值类型是"type"
var variable_name type = value
同时初始化多个变量
/*
定义三个类型都是"type"的三个变量,并且他们分别初始化相应的值
vname1为v1vname2为v2vname3为v3
*/
var vname1,vname2,vname3 type= v1, v2, v3
你是不是觉得上面这样的定义有点复杂没关系因为go语言的设计者也发现这样复杂了我们来让它变得简单一点我们可以直接忽略类型这个申明那么上面的什么变成了如下
/*
定义三个变量,他们分别初始化相应的值
vname1为v1vname2为v2vname3为v3
然后他们的类型自动根据初始化的值来确定相应的类型go会帮你做这件事
*/
var vname1,vname2,vname3= v1, v2, v3
你还是觉得上面的复杂?好吧,我也觉得是,让我们继续简化
/*
定义三个变量,他们分别初始化相应的值
vname1为v1vname2为v2vname3为v3
编译器会根据初始化的值自动推导出相应的类型
*/
vname1,vname2,vname3 := v1, v2, v3
现在是不是看上去非常的简单了,`:=`这个定义直接替代了`var``type`,这样的代码是不是很简洁,但是:=有一个限制,那就是它只能用在函数内部,在函数外部使用则不能编译通过。比如用以定义全局变量。
一个特殊的变量名是`_`下划线。任何赋给它的值都被丢弃。在这个例子中将赋值35赋值给b同时丢弃34。
_, b := 34, 35
Go的编译器对申明却未使用的变量会在编译阶段报错。下面的代码会产生一个错误申明了i却未使用
package main
func main() {
var i int
}
##常量
所谓常量也就是在编译阶段就确定下来的值程序运行时无法改变该值GO程序里面常量定义可以是数字类型、Bool、字符串
它的语法如下
const constant_name = value
下面是一些申明的例子
const PI = 3.1415927
const i = 10000
const Max_Thread = 10
const prefix = 'astaxie_'
当然如果需要,可以明确指定常量的类型:
const PI float32 = 3.1415927
##内置基础类型
###Boolean
对于布尔值在GO中, 它的类型是:`bool` 可以有的值是:`true``false`。默认为`false`
//示例代码
var isactive bool
var enabled, disabled = true, false //忽略类型的申明
func test(){
var available bool //一般的申明
valid := false //忽略var和type的什么
available = true //赋值操作
}
###数字类型
对于整数类型有无符号和有符号两种GO同时支持`int``uint`这两种类型的长度相同但具体长度取决于编译器的实现。当前的gc和gccgo编译器在32位和64位平台上都使用32位来表示`int``uint`但未来在64位平台上可能增加到64位。GO里面也有直接定义好位数的类型`rune`, `int8`, `int16`, `int32`, `int64``byte`, `uint8`, `uint16`, `uint32`, `uint64`。目前`rune``int`的别称,将来会改为`int32`的别称。`byte``uint8`的别称。
>注意一点就是这些类型之间的变量不允许相互之间赋值、操作,不然会引起编译器的错误。
>
>如下的代码会出现错误
>
> var a int8
>
> var b int32
>
> c:=a + b
浮点类型的值有`float32``float64`(没有`float`类型)。
这就是全部吗NOGo支持复数。它的变量类型是`complex128`64位实数64位虚数。如果需要小一些的还有`complex64`(32位实数32位虚数)。复数写为`re + imi``re`是实数部分,`im`是虚数部分,而`i`是标记。使用复数的一个例子:
var c complex64 = 5+5i;
fmt.Printf("Value is: %v", c)
将会打印:(5+5i)
###字符串
前面一节里面说过字符串都是UTF-8类型的字符串通过一对双引号(")或反引号(`来定义它的类型是string
//示例代码
var french_hello string //声明变量为字符串的一般方法
var empty_string string = "" // 申明了一个字符串变量,初始化为空值
func test(){
no, yes, maybe := "no", "yes", "maybe" //忽略var和type的申明同时申明多个变量
japanese_hello := "Ohaiou" //同上
french_hello = "Bonjour" //常规赋值
}
在Go中字符串是不可变的例如如下的代码编译时会报错
var s string = "hello"
s[0] = 'c'
那么如果真的想要修改怎么办呢?如下的代码可以实现
s := "hello"
c := []byte(s)
c[0] = 'c'
s2 := string(c)
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.Println("%s\n", s)
如果要申明一个多行的字符串怎么办?可以通过`` ` ``来申明
m:=`hello
world`
`` ` `` 括起的字符串为Raw字符串即字符串在代码中的形式就是打印时的形式没有字符转义换行也将原样输出。
###错误类型
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数据结构的文章大家可以看到这些基础类型底层都是开辟了一块内存然后存了相应的值
![](images/2.2.basic.png?raw=true)
##一些技巧
###分组定义
Go语言里面针对多个同时申明变量、常量或者import多个包的时候可以采用分组的方式来申明
如下的代码
import "fmt"
import "os"
const i = 100
const pi = 3.1415
const prefix = "go_"
var i int
var pi float32
var prefix string
可以通过分组的方式写成如下
import(
"fmt"
"os"
)
const(
i = 100
pi = 3.1415
prefix = "go_"
)
var(
i int
pi float32
prefix string
)
>除非被显式设置为其他值或iota, 每个const分组的第一个常量被默认设置为它的0值,第2及后续的常量被默认设置为他前面那个常量的值如果前面那个常量的值是iota则它也被设置为iota
###iota枚举
GO里面有一个关键字`iota`这个关键字用来申明enum的时候采用它默认开始值是0每调用一次加1
const(
x = iota //x == 0
y = iota //y == 1
z = iota //z == 2
w // 常量申明省略值时默认和之前一个值的字面相同。这里隐示的说w = iota,因此w==3, 其实上面y和z可以同样不用=iota
)
const v = iota // 每遇到一个const关键字iota被重置此时 v==0
##array、slice、map
###array
array就是数组它的定义如下`[n]type`,n表示数组的长度type表示存储内容的类型
对数组的操作和其他语言类似,都是通过`[]`来进行读取和赋值
var arr [10]int //申明了一个int类型的数组
arr[0] = 42 //数组下标是从0开始的
arr[1] = 13 //赋值操作
fmt.Printf("The first element is %d\n", arr[0]) //获取数据
由于数组的长度也是类型的一部分,比如`[3]int`与`[4]int`是不同的类型,所以数组是不能改变长度的,而且数组之间的赋值是值赋值,当把一个数组作为一个参数传入函数的时候,是这个数组的副本,而不是该数组的指针,如果要使用指针,那么就需要用到下面介绍的`slice`。
数组申明可以使用另一种`:=`来申明
a := [3]int{1,2,3} //申明一个长度为3的数组
b := [10]int{1,2,3} //申明了一个长度为10的数组其中前面三个元素初始化为1、2、3其他默认为0
c := […]int{4,5,6} //可以省略长度采用…go会自动计算长度
也许你会说我想数组里面还是数组能实现吗当然咯GO支持嵌套数组即多维数组如下代码申明了一个二维数组
//申明了一个二维数组该数组是一个两个元素的数组然后每个元素里面是4个int的元素
double_array := [2][4]int {[4]int{1,2,3,4}, [4]int{5,6,7,8}}
//如果内部的元素和外部的一样,那么上面的申明可以简化,直接忽略内部的类型
easy_array :=[2][4]int{{1,2,3,4},{5,6,7,8}}
数组的分配如下所示:
![](images/2.2.array.png?raw=true)
###slice
在很多的应用场景里面,数组不能满足我们的需求,因为在刚开始的时候,我们不知道到底需要多大的数组合适,所以我们需要动态数组,在go里面这种数据结构叫`slice`
`slice`并不是真正意义上面的动态数组,而是一个引用类型,`slice`总是指向底层的一个`array`。`slice`的申明也可以像`array`一样只要省略size
//和申明array一样只是少了长度
var fslice []int
接下来我们可以申明一个slice并初始化数据如下所示
slice := []byte {'a', 'b', 'c', 'd'}
slice可以从一个数组或者一个已经存在的slice里面再次申明`slice`通过array[i:j]来获取i是数组的开始位置j是结束位置但不包含array[j]他的长度是j-i
//申明一个含有十个字符元素的数组
var ar[10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
//申明两个含有byte的slice
var a,b[]byte
//a指向数组的第2个元素开始并到第五个元素结束
a = ar[2:5]
//现在a含有的元素: ar[2], ar[3] and ar[4]
// b是数组ar的另一个slice.
b = ar[3:5]
// b的元素是: ar[3], ar[4]
注意slice和数组申明时的区别申明数组时方括号内写明了数组的长度或使用`...`自动计算长度而申明slice时方括号内没有任何字符
他们的数据结构如下所示
![](images/2.2.slice.png?raw=true)
slice有一些简便的操作
- slice的默认开始位置是0ar[:n]等价于ar[0:n]
- slice的第二个序列默认是数组的长度ar[n:]等价于ar[n:len(ar)]
- slice如果从一个数组里面直接获取可以这样ar[:]因为默认第一个序列是0第二个是数组的长度即等价于ar[0,len(ar)]
下面这个例子展示更多关于slice的操作
//申明一个数组
var array [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
//申明两个slice
var a_slice,b_slice
// 演示一些简便操作
a_slice = array[:3] // 等价于a_slice = array[0:3] a_slice包含元素: a,b,c
a_slice = array[5:] // 等价于a_slice = array[5:9] a_slice包含元素: f,g,h,i,j
a_slice = array[:] // 等价于a_slice = array[0:9] 这样a_slice包含了全部的元素
//从slice获取slice
a_slice = array[3:7] // a_slice包含元素: d,e,f,glen=4cap=8
b_slice = a_slice[1:3] // b_slice 包含a_slice[1], a_slice[2] 也就是含有: e,f
b_slice = a_slice[:3] // b_slice 包含 a_slice[0], a_slice[1], a_slice[2] 也就是含有: d,e,f
b_slice = a_slice[0:5] // 对slice的slice可以在cap范围内扩展此时b_slice包含c,d,e,f,g
b_slice : a_slice[:] // b_slice包含所有a_slice的元素: d,e,f,g
slice是引用类型所以当引用改变其中项目的值的时候那么其他的所有引用都会改变该值例如上面的a_slice和b_slice如果修改了a_slice中项目的值那么b_slice相对应的值也会改变。
从概念上面来说slice像一个结构体这个结构体包含了三个元素
- 一个指针指向数组中slice指定的开始位置
- 长度slice的长度
- 最大长度也就是slice开始位置到数组的最后位置的长度
array := [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
slice := A[4:8]
上面代码的真正存储结构如下图所示
![](images/2.2.slice2.png?raw=true)
slice下面有几个有用的内置函数
- len 获取slice的长度
- cap 获取slice的最大容量
- append 向slice里面追加一个或者多个元素然后返回一个和slice一样类型的slice
- copy 函数copy 从源slice src 复制元素到目标dst并且返回复制的元素的个数
append函数会改变slice所引用的数组的内容从而影响到引用同一数组的其它slice。
但当slice中没有剩余空间(cap-len) == 0)时此时将动态分配新的数组空间返回的slice的数组指针将指向这个空间而原数组的内容将保持不变其它引用此数组的slice不受影响。
###map
map也就是python中字典的概念它的格式`map[keyType]valueType`
我们看下面的代码map的读取和设置也类似slice一样通过key来操作只是slice只有int的key而map多了很多类型可以是int可以是string及所有完全定义了"=="与"!="操作的类型
// 申明一个key是字符串值为int的字典
var numbers map[string] int
//另一种map的申明方式
numbers := make(map[string]int)
numbers["one"] = 1 //赋值
numbers["ten"] = 10 //赋值
numbers["three"] = 3
fmt.Println("第三个数字是: ", numbers["three"]) //读取数据
// 打印出来如下:
//第三个数字是: 3
这个map就像我们平常看到的表格一样左边列是key右边列是值
使用map过程中需要注意的几点
- map是无序的每次打印出来的map都会不一样,他不能通过index获取而必须通过key获取
- map的长度是不固定的也就是和slice一样也是一种引用类型
- 内置的len函数同样试用于map返回map拥有的key的数量
- map的值可以很方便的修改通过`numbers["one"]=11`可以很容易的把key为`one`的字典值改为11
map的初始化可以通过key:val的方式初始化值同时map内置有判断是否存在key的方式
删除map的元素通过delete
//初始化一个字典
rating := map[string]float32 {"C":5, "Go":4.5, "Python":4.5, "C++":2 }
//map可以有两个返回值第二个返回值如果不存在key那么ok为false如果存在ok为true
csharp_rating, ok := rating["C#"]
if ok {
fmt.Println("C# is in the map and its rating is ", csharp_rating)
} 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`用于内建类型(`map`、`slice` 和`channel`)的内存分配。`new`用于各种类型的内存分配。
内建函数new 本质上说跟其他语言中的同名函数功能一样new(T) 分配了零值填充的T 类型的内存空间,并且返回其地址,一个*T类型的值。用Go的术语说它返回了一个指针指向新分配的类型T的零值。有一点非常重要new返回指针。
内建函数make(T, args)与new(T)有着不同的功能。它只能创建slicemap和channel并且返回一个有初始值(非零)的T类型而不是*T。本质来讲导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如一个slice是一个包含指向数据内部array的指针长度和容量的三项描述符在这些项目被初始化之前slice 为nil。对于slicemap 和channelmake 初始化了内部的数据结构填充适当的值。make返回初始化后的非零值。
下面这个图详细的解释了new和make之间的区别
![](images/2.2.makenew.png?raw=true)
## links
* [目录](<preface.md>)
* 上一章: [你好,GO](<2.1.md>)
* 下一节: [流程和函数](<2.3.md>)
## LastModified
* $Id$