000_关于环境
Golang的官网是https://golang.org,不过因为众所周知的原因,国内不能正常访问,那么替代方案有这么几种:
- 方案一:Google CN提供的镜像网站:该网站最大的好处就是原版英文,可以获得等同于原官网的体验
- 方案二:中文版官网:该网站已经部分汉化了,可以帮助阅读英文有困难的同学们
- 方案三:使用Golang起一个本地服务:安装Golang后,在命令行里输入
godoc -http=:8080
,并在浏览器访问http://localhost:8080/
即可访问本地官网(英文),8080是端口号,可以更改,这种方式优点在于不依赖网络 - 方案四:https://tip.golang.org/是golang.org的完全限定域名,由Google提供,目前中国大陆可以正常访问
跨平台编译
Golang有两个重要的环境变量GOOS
和GOARCH
,其中GOOS
指的是目标操作系统,它的可用值为:
- darwin
- freebsd
- linux
- windows
- android
- dragonfly
- netbsd
- openbsd
- plan9
- solaris
一共支持10中操作系统。GOARCH
指的是目标处理器的架构,目前支持的有:
- arm
- arm64
- 386
- amd64
- ppc64
- ppc64le
- mips64
- mips64le
- s390x
一共支持9中处理器的架构
如果我们要生成不同平台架构的可执行程序,只要改变这两个环境变量就可以了(临时禁用CGO,以防出问题),比如要生成linux
64位
的程序,命令如下:
1 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build github.com/xxx/hello |
前面两个赋值,是更改环境变量,这样的好处是只针对本次运行有效,不会更改我们默认的配置。
以上这些用法差不多够我们用的了,更多关于go build
的用法,可以通过以下命令查看:
1 | go help build |
此段摘自 飞雪无情 的博客
001_数据类型
rune类型
Golang没有Java里面的char
类型,取而代之的是rune
类型,Golang源码对该类型的注释是这样的:
rune is an alias for int32 and is equivalent to int32 in all ways.
译:rune是int32的别名,在所有方面都等同于int32。
因此,它在执行以下代码的时候,输出的结果并不是预期的字符a
,而是数字97
:
1 | func main() { |
这个地方的对应关系是基于ASCII
码表的,myChar
是一个rune
类型变量,而rune
其实就是代表整数的int32
类型,参照码表,a
对应为十进制下的97
,所以这里的输出结果是数字97
。
要输出字符a
的话,需要进行类型转换,将rune
类型转换为字符串类型的a
:
1 | func main() { |
正因为这个特点,我们大概会遇到这个困扰:
如何将一个数字,例如:97
转换为字符串使用?
如果我们套用Python
或Java
等语言的经验的话,大概会写出这样的代码:
1 | func main() { |
这样显然是不符合我们的预期的,而正确的方法是这样的:
1 | func main() { |
string类型
忽视转义字符,以纯文本输出
使用键盘1左边的键`
而不是双引号,就可以实现对转义字符
以纯文本输出
1 | var name = `Ja\nck` |
输出
1 | Ja\nck |
字符串的遍历
string是字符串类型,采用unicode
编码,因此在表示非ASCII
字符的时候,其大小不是一个字节,具体看示例:
1 | func main() { |
因此,我们在遍历的时候,不能直接使用其他编程语言里面常用的按照长度遍历,否则会是这样的结果:
1 | func main() { |
输出:
1 | 230 |
使用string()
转换类型:
1 | func main() { |
输出就是彻底的乱码:
1 | æ |
出现这个问题的原因就是,汉字是由多个字节来表示的,而Golang当中的len()
是获取的字节长度,而非字符数
正确遍历一个字符串中每一个字符的方法:
1 | func main() { |
输出:
1 | 0 我 |
注意:前者的下标依然是按照字节来计算的,而后者则是按照字符
计算真实长度的方法:
1 | func main() { |
在不使用range
的情况下遍历:
1 | func main() { |
输出:
1 | 我 |
其他字符串操作常用函数
strings.Fields
:按空格分隔
1 | func main() { |
int类型
关于内存地址
Golang当中的int
类型的变量,在值改变的时候,其内存地址不会改变
具体示例如下:
例1:
1 | func main() { |
输出为:
1 | i的值1,i的地址0xc000094000 |
例2:
1 | func main() { |
输出为:
1 | 0xc000094028 |
数组和切片
数组和切片的不同
- 切片是类似指针的引用类型,所以可以直接打印地址,而不需要取地址符号
&
- 数组定义时就会分配内存空间;切片不会
1 | func main() { |
- 数组可以和相同类型的数组直接比较,切片只能和
nil
作比较,刚刚定义的切片为nil
1 | func main() { |
1 | invalid operation: s1 == s2 (slice can only be compared to nil) |
切片的扩容
当容量小于1024,当长度超出容量,则容量翻倍,如果翻倍后容量已经大于长度,则容量就是这个数字;如果容量依然小于长度,则容量等于长度
切片的删除元素
Golang
并没有提供删除的相关功能,但是我们可以利用切片
和append
实现
这是一个遍历集合,并删除特定元素的例子:
1 | func main() { |
方式1:
1 | func main() { |
输出:
1 | 初始切片: [1 2 3 4 5] |
分析:
由于newSlice
是从初始切片截取所得,其拥有相同的初始索引位置(索引位置:0)的值(值:1),索引newSlice
的内存地址和原始切片的内存地址是相同的
切片的内存地址等于其第一个元素的内存地址,因为此处我们是从0号索引截取,所以
s
和newSlice
的内存地址相同
因此,当我们对newSlice
做append
操作时,原切片的值也被一起改变了,这并不好
方式2:
解决上述问题的思路就是,我们确实需要一个切片,它的初始元素等于原切片的初始元素,而内存地址不同,我们即想到了创建一个新的slice,而不是在原有slice(或其切片上)上直接操作
我们可以运用copy
函数,来拷贝需要的值
1 | func main() { |
输出
1 | 初始切片: [1 2 3 4 5] |
关于Set和去重
Golang
是不提供set
这种数据类型的,但是这也并没有什么,因为set
完全就可以通过使用map
来实现
我们在其他语言中,set
最常用的就是去重,在Golang
中,我们这样写即可:
1 | func main() { |
注意:无论是map
还是set
,都无法保证集合内元素的顺序和插入时的顺序一致,甚至Golang
还刻意提高了其顺序的随机性
002_关于对象声明
Golang中常规变量声明有两种方式:
方式一:使用var
关键字声明:
1 | var name string |
方式二:使用:=
的方式声明
1 | name := "" |
不过值得注意的是,这两种方式还是存在区别的:
var
关键字声明可以被用在全局位置。也就是说,:=
这种方式只能够在函数内部使用- 我们知道Golang中的变量一经声明必须使用,否则会抛出
xxx declared and not used
的错误,不过全局变量可以声明而不使用
003_关于对象的初始化
尝试思考下面代码的执行结果,并分析原因:
1 | func main() { |
输出:
1 | panic: runtime error: invalid memory address or nil pointer dereference |
原因是这样的:当我们使用引用类型的时候,必须先分配其内存空间,然后我们存储的值才能有地方放
所以需要像这样:
1 | func main() { |
Golang源码中的new
1 | // The new built-in function allocates memory. The first argument is a type, |
make
也是用于内存分配的,但是和new不同,它只用于chan
、map
以及切片
的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
make和new的异同
- 二者都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零
- make返回的还是这三个引用类型本身;而new返回的是指向类型的指针
其实new不常用
所以有new这个内置函数,可以给我们分配一块内存让我们使用,但是现实的编码中,它是不常用的。我们通常都是采用短语句声明以及结构体的字面量达到我们的目的,比如:
1 | i:=0 |
这样更简洁方便,而且不会涉及到指针这种比麻烦的操作。
make
函数是无可替代的,我们在使用slice
、map
以及channel
的时候,还是要使用make进行初始化,然后才才可以对他们进行操作。
004_for…range
当需要在循环中改变值
当我们试图在for...range
中改变一个切片的值的时候,会遇到这个问题:
1 | func main() { |
输出:
1 | [1 2 3 4 5] |
其原因是value
只是遍历出来的值的拷贝,要对其进行修改,需要使用下标:
1 | func main() { |
输出:
1 | [1 2 9 4 5] |
当需要在循环中存储值
当我们试图存储被for...range
遍历的内容的引用时,可能会出现这样的问题:
1 | func main() { |
输出:
1 | key = 0, value = 5 |
原因其实很简单:value
虽然在遍历过程中,其值一直在修改,但是它的内存地址其实一直都没有变,所以在最后一次循环前放入myMap
的引用所存储的值也都被修改为最后的5
了
解决方法:其实也很简单,每次循环时创建一个新的变量来存储value
的值就可以了,这样放入myMap
的内存地址每次也都是不同的
1 | func main() { |
输出:
1 | key = 2, value = 3 |
当在循环中遇到闭包
如果我们执行以下代码:
1 | func main() { |
则会输出:
1 | 5 |
为什么不是输出1 2 3 4 5
呢?因为,在匿名函数中,fmt.Println(value)
语句的value
不是该匿名函数内声明的,而是从外层函数那里获得的引用
没错,闭包内的变量和闭包外的是相同的内存地址,而不是值拷贝
005_关于常量
- 数值类型常量如果不明确指定类型可以直接运算(如:float64和int32)
- 常量可以声明但不使用
- 常量的声明只需要使用
=
,而不是:=
或者var
关于iota
注意:iota
无论你是否使用,其值都是从0依次递增,直到遇到新的const
,iota
的值即重制为0
例:
1 | func main() { |
输出
1 | a = 9 |