前言
学习任何知识都会有一个学习背景
最近,我们团队乃至我司整个云服务,上go的呼声越来越高!新服务已经开始用go开发,部分现有Java版的服务重构为go也只是时间问题而已,故相关技术积累势在必行!在云网络的分布式服务乃至在一切高并发,分布式后台服务中,golang都有着很大的优势。
据我对国内互联网行业的实际考察,了解,目前国内主流互联网公司都在积极投入go的怀抱……
青云更是全栈使用了go……
还有火的一塌糊涂的docker。
它为云而生。
它为并发而生。
还有go的安全、简洁、高效
有良好的Java、C/C++背景,拿起go很容易。
……
参考:
2017年值得学习的三大编程语言
2017年编程语言排行榜,相比2016,go的飞速上升趋势很明显。而Java有很大份额的缩减……
当然,当前在企业级应用开发中,Java依然占有一席之地。
而个人认为:一个优秀的研发工程师乃至架构师……是不能仅仅局限于一门语言的,语言只是拿来就用的工具,关键还是实战经验和行业内功的修炼。但是对于任何编程语言,不学也不是凭空就会的。
简单说go的历史和应用
历史和概况
目前流行的go应用
go的开发环境搭建
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
安装go
brew install go
看安装成功之后的提示就能知道,go的运行需要一个环境变量——GOPATH,是go中非常重要的一个环境变量,必须配置好,假如不配,那么安装go的一些工具,或者自己写的包,就会出问题。当然还有GOROOT。
PS:其实在目前的go版本中,这些环境变量已经在安装的时候默认被配置ok了。只需要在机器的环境配置文件中做如下操作即可:
You may wish to add the GOROOT-based install location to your PATH: export PATH=$PATH:/usr/local/opt/go/libexec/bin ==> Summary �� /usr/local/Cellar/go/1.9: 7,639 files,293.7MB
- src 存放源代码(比如:.go .c .h .s,以及下载安装的一些工具包)
- pkg 编译后生成的文件(比如:.a)
- bin 编译后生成的可执行文件(为了方便,可以把此目录加入到 $PATH 变量中)
下面查看go的环境变量配置,看看配置的情况如何了。
发现查看环境变量的是go env命令
bash-3.2$ go env GOARCH="amd64" GOBIN="" GOEXE="" GOHOSTARCH="amd64" GOHOSTOS="darwin" GOOS="darwin" GOPATH="/Users/dashuai/go" GORACE="" GOROOT="/usr/local/Cellar/go/1.9/libexec" GOTOOLDIR="/usr/local/Cellar/go/1.9/libexec/pkg/tool/darwin_amd64" GCCGO="gccgo" CC="clang" GOGCCFLAGS="-fPIC -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build746511617=/tmp/go-build -gno-record-gcc-switches -fno-common" CXX="clang++" CGO_ENABLED="1" CGO_CFLAGS="-g -O2" CGO_CPPFLAGS="" CGO_CXXFLAGS="-g -O2" CGO_FFLAGS="-g -O2" CGO_LDFLAGS="-g -O2" PKG_CONFIG="pkg-config"
发现GOROOT、GOPATH等环境变量确实已经在安装go 1.9 时,被默认设置。
然后,按照之前安装go成功之后的提示,在.bash_profile文件中进行如下配置:
export PATH=$PATH:/usr/local/opt/go/libexec/bin
最后,source下即可。
PS:各个环境变量含义
$GOROOT
表示go的安装位置,它的值一般都是 $HOME/go,当然也可以安装在别的地方,这个都无所谓的。$GOARCH
表示目标机器的处理器架构。$GOOS
表示目标机器的操作系统,它的值可以是 darwin,freebsd,linux 或 windows。$GOBIN
表示编译器和链接器的安装位置,默认是 $GOROOT/bin,如果使用的是 Go 1.0.3 及以后的版本,一般情况下可以将它的值设置为空,Go 将会使用前面提到的默认值。$GOPATH
表示工作的环境路径,允许包含多个目录。当有多个目录时,Windows分隔符是分号,Linux是冒号,当有多个GOPATH时,默认会将go get
命令的内容放在第一个目录下。$GOPATH
目录约定有三个子目录:
go的IDE
hello word!
1 package main 2 import "fmt" 3 4 /* 5 注释风格和C/C++,JAVA类似 6 */ 7 func main() { 8 fmt.Println("hello world!"); //打印hello world! 9 } 10
wangyishuaideMacBook-Pro:goStu wangyishuai$ go build hello.go wangyishuaideMacBook-Pro:goStu wangyishuai$ ls hello hello.go wangyishuaideMacBook-Pro:goStu wangyishuai$ ./hello hello world!
当然,图省事,直接使用go run命令执行也是ok的,但是这样不会生成任何中间结果。
wangyishuaideMacBook-Pro:goStu wangyishuai$ rm hello wangyishuaideMacBook-Pro:goStu wangyishuai$ ls hello.go wangyishuaideMacBook-Pro:goStu wangyishuai$ go run hello.go hello world! wangyishuaideMacBook-Pro:goStu wangyishuai$ ls hello.go
有一个小坑,如果import的包不用,编译会报错。比如,fmt包导入了就必须使用,类似的包括变量等。当注释掉第8行代码,在执行会报错,这点严谨性,比Java强多了。
wangyishuaideMacBook-Pro:goStu wangyishuai$ go run hello.go # command-line-arguments ./hello.go:2:8: imported and not used: "fmt"
go的常用关键字(25个)
- var和const :变量和常量的声明
- var varName type 或者 varName : = value
- package and import: 导入
- func: 用于定义函数和方法
- return :用于从函数返回
- defer someCode :在函数退出之前执行
- go : 用于并行
- select 用于选择不同类型的通讯
- interface 用于定义接口
- struct 用于定义抽象数据类型
- break、case、continue、for、fallthrough、else、if、switch、goto、default 流程控制
- chan用于channel通讯
- type用于声明自定义类型
- map用于声明map类型数据
- range用于读取slice、map、channel数据
如上,其实和其他大部分语言都差不多。其中go、chan、以及select是go的特色和重点,又比如map类似Java的hashmap……其他的也没什么新鲜的,接下来快速过。
go程序的组织模式
go程序类似Java,都是通过package组织,然后每个独立的、可运行的go程序的入口必须都是main函数,也就是main包里的main函数。
变量和常量
使用var关键字
var是go最基本的定义变量方式 ,但是它的变量类型是放在了变量名后面。
如下,格式化打印语句
Printf
和C语言非常像,其实go就是C语言衍生改进而来,这也是为什么称它为互联网世界中的C语言的原因。
package main import ( "fmt" ) func main() { var x int; x = 1; fmt.Printf("%d",x); }
简洁的定义
有时候会觉得总是多写一个var比较烦,那么可以用:=来定义变量(个人感觉也不是想象中的那么简洁……)
package main import ( "fmt" ) func main() { var x int; x = 1; fmt.Printf("%d",x); a,b,c := 1,2,3; // 自动判断类型,类似Python fmt.Println(a,c); }
这种方式类似Python,省去了写类型的步骤,在go程序中十分常见。但是这样的写法有一个缺点::=无法用于函数体外部,而var可以。故var也是有存在的必要的。
注意:
1、package和import必须挨着,也就是它们中间不能有其他代码,否则报错。
2、对于没有显式初始化的变量,Go语言总是将零值赋值给该变量。
3、在Go语言中,声明变量的时候,类型名总是在变量名的后面。
const常量
没什么可说的,和C语言一样的用法,唯一不同的就是定义常量时,加类型或者不加都可以,但是注意,最好养成习惯,类型加在常量名后面。
func main() { fmt.Println(x); const dashuai string = "dashuai"; const gogogo = 888; }
而且,字符串数据类型和Java不太一样,是完全小写的string。下面会说到。
注意:
1、当需要设置多个常量的时候,不必重复使用const
,可以使用小括号设置 (var同理
)
package main import ( "fmt" ) func main() { var ( a int; b string; c bool; ) fmt.Println(a,c) // 0 false const ( aa = 1 bb = "dada" cc = true ) fmt.Println(aa,bb,cc) }
数据类型
bool byte complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr
无类型的数值常量可以兼容go内置的任何类型的数值
在不同类型数值之间进行运算或者比较操作,需要进行类型转换。类型转换采用type(value)
的方式,只要合法就能转换成功,即使会导致数据精度丢失。
快速过一下,和Java对比,如下常用的几个:
整型
1、go独有的 rune,其实就是代表了无符号32位整型 unit32。
2、有符号整型 int (依赖于os,可能是int32或者int64)
- int8 [-128,127]
- int16 [-32768,32767]
- int32[-2147483648,2147483647]
- int64[-9223372036854775808,9223372036854775807]
也就是说,go是见名知意,go中可以直接定义xxx位的整型。
3、字节 byte,等价于 uint8
4、无符号整型 uint ,依赖于不同os实现,可以是uint32或者uint64
- uint8 [0,255]
- uint16[0,65535]
- uint32[0,4294967295]
- uint64[0,18446744073709551615]
- uintptr一个可以恰好容纳指针值的无符号整型(对32位平台是uint32,对64位平台是uint64)
可以通过unsafe.Sizeof
函数实现类似C语言里sizeof
操作符的功能,查看类型的字节长度。
package main import ( "fmt" "unsafe" ) func main() { str := "dashuai" fmt.Println(unsafe.Sizeof(str)) // 16 a := 1 fmt.Println(unsafe.Sizeof(a)) // 8 var b int8 = 1 fmt.Println(unsafe.Sizeof(b)) // 1 var c int32 = 1 fmt.Println(unsafe.Sizeof(c)) // 4 var d int64 = 1 fmt.Println(unsafe.Sizeof(d)) // 8 }
浮点类型
1、float32 ,±3.402 823 466 385 288 598 117 041 834 845 169 254 40x1038计算精度大概是小数点后7个十进制数
2、float64,±1.797 693 134 862 315 708 145 274 237 317 043 567 981x1038计算精度大概是小数点后15个十进制数
布尔类型 bool(取值true false)
注意区分Java的boolean/Boolean写法,其实是和C语言一样,这里也说下,学习go,就对比着C语言和Java语言,这样用起来是很快的。
布尔值使用内置的true和false。Go语言支持标准的逻辑和比较操作,这些操作的结果都是布尔值。还可以通过!b
的方式反转变量b
的真假。也就是逻辑非。
go可直接定义复数
这也是go独有的, complex32、complex64,在针对一些算法的实现时,非常方便,比如傅里叶变换。
func main() { var x complex64 = 10 + 9i; fmt.Print(x + x); }
complex32复数,实部和虚部都是float32
complex64复数,实部和虚部都是float64
字符串
常用是string定义,Go语言中的字符串是UTF-8编码格式(当字符为 ASCII 码时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。
UTF-8 是被广泛使用的编码格式,是文本文件的标准编码,其它包括 XML 和 JSON 在内,也都使用该编码。由于该编码对占用字节长度的不定性,Go 中的字符串也可能根据需要占用 1 至 4 个字节,这与C++、Java 或者 Python 不同。
Go 这样做的好处是:不仅减少了内存和硬盘空间占用,同时也不用像其它语言那样需要对使用 UTF-8 字符集的文本进行编码和解码。
Go语言中字符串的可以使用双引号( " )或者反引号( ` )来创建。
双引号用来创建可解析的字符串,所谓可解析的是指字符串中的一些符号可以被格式化为其他内容,如 "\n" 在输出时候会被格式化成换行符, 如果需要按照原始字符输出必须进行转义。
反引号创建的字符串原始是什么样,那输出还是什么,不需要进行任何转义。
package main import ( "fmt" ) func main() { str := "123" str1 := "\n" str2 := `\n` str3 := "\\n" fmt.Println(str) // 123 fmt.Println(str1) // 换行 fmt.Println(str2) // \n fmt.Println(str3) // \n }
注意
1、Go语言中的部分转义字符
\\ 反斜线
\' 单引号
\" 双引号
\n 换行符
\uhhhh 4个16进制数字给定的Unicode字符
2、在Go语言中单个字符可以使用单引号 ' 来创建。而且,一个单一的字符可以用一个单一的rune
来表示(rune是uint32类型)。这也是容易理解的,因为Go语言的字符串是UTF-8编码,其底层使用4个字节表示,也就是32 bit。
格式化字符串
目前为止,使用了fmt.Printf
和fmt.Prinfln
,对于前者的使用,就像C语言中的printf函数一样,常用的格式化指令如下:
格式化指令 | 含义 |
---|---|
%% | %字面量 |
%b | 一个二进制整数,将一个整数格式化为二进制的表达方式 |
%c | 一个Unicode的字符 |
%d | 十进制数值 |
%o | 八进制数值 |
%x | 小写的十六进制数值 |
%X | 大写的十六进制数值 |
%U | 一个Unicode表示法表示的整形码值,默认是4个数字字符 |
%s | 输出以原生的UTF-8字节表示的字符,如果console不支持UTF-8编码,则会输出乱码 |
%t | 以true或者false的方式输出布尔值 |
%v | 使用默认格式输出值,或者使用类型的String()方法输出的自定义值,如果该方法存在的话 |
%T | 输出值的类型 |
常用的格式化指令修饰符如下:
空白
如果输出的数字为负,则在其前面加上一个减号"-"。如果输出的是整数,则在前面加一个空格。使用%x或者%X格式化指令输出时,会在结果之间添加一个空格。例如fmt.Printf("% X","实")输出E5 AE 9E#
+
让格式化指令在数值前面输出+号或者-号,为字符串输出ASCII字符(非ASCII字符会被转义),为结构体输出其字段名-
让格式化指令将值向左对齐(默认值为像右对齐)0
让格式指令以数字0而非空白进行填充
字符串处理包
strings
包提供了如查找字符串,分割字符串,判断前后缀,判断字符串包含,字符串替换,统计字符串出现的次数等常用操作。
strconv
包提供了许多可以在字符串和其他类型的数据之间进行转换的函数。例如可以将数字转换为字符串,将数字样式的字符串转换为数值(将字符串"12345"
转换int
类型的整数)
这些直接查文档就可以了。
数组 array slice
array可以类比Python的元组,slice可以类比Python的列表,Java的list,很好理解。
package main import ( "fmt" ) /* array用法 */ func main() { var x [10]int; x[0] = 100; x[8] = 800; // %v 可以直接遍历打印数组,非常方便,C语言是没有的。非常高级的用法%v,意思是value,同样数组也不能越界使用。 // 如果没有初值,自动设为0 fmt.Printf("%v \n",x); // 看长度,和Python的len函数一样 fmt.Println(len(x)); var str [5]string; str[3] = "aaaa"; // 默认初值为空串 fmt.Printf("%v \n",str); // 如果使用简洁声明,那么可以直接赋值 y := [3]int{1,3} fmt.Printf("%v \n",y); }
在看下slice,其实它就是一个动态数组
var x [10]int; // 这是一个静态数组,array,之前说过了 var slice_x []int; // 非常明显,不需要显式声明长度
还可以通过从一个数组或在一个已经存在的slice中再次声明slice
xx := [10]int{1,3,4,5,6,7,8,9,10}; // 一个静态数组 z := xx[1:3]; //对xx切片(和Python一样的道理),切片范围是前闭后开区间,也就是截取了2,3。然后截取的结果赋值给数组z,go的数组同样从下标0开始
其实,现在的z就变为了动态数组slice,类似于指针。
同样slice也可以用简洁方式声明,并且配合关键字make去分配内存空间,make用于内建类型(map、slice 和channel)的内存分配,有些类似C语言的malloc。
package main import ( "fmt" ) func main() { x := make([]int,8); // 长度为3,容量为8 fmt.Printf("%v \n",x); // [0 0 0] // 会自动扩容 x = append(x,1,8); fmt.Printf("%v \n",x); // [0 0 0 1 2 3 4 5 6 7 8] fmt.Printf("%v %v \n",len(x),cap(x)); // 11 16 }
小结
1、总的来说,如下图,可以从一个array声明slice,其实就是指针指向。
2、在Go语言中,字符串支持切片操作,但是需要注意的是:
如果字符串都是由ASCII字符组成,那可以随便使用切片进行操作
如果字符串中包含其他非ASCII字符,直接使用切片获取想要的单个字符时需要十分小心,因为对字符串直接使用切片时是通过字节进行索引的,但是非ASCII字符在内存中可能不是由一个字节组成。如果想对字符串中字符依次访问,可以使用range
操作符。
另外获取字符串的长度可能有两种含义,一种是指获取字符串的字节长度,一种是指获取字符串的字符数量。
3、字符串支持以下操作:
语法 | 描述 |
---|---|
s += t | 将字符串t追加到s末尾 |
s + t | 将字符串s和t级联 |
s[n] | 从字符串s中索引位置为n处的原始字节 |
s[n:m] | 从位置n到位置m-1处取得的字符(字节)串 |
s[n:] | 从位置n到位置len(s)-1处取得的字符(字节)串 |
s[:m] | 从位置0到位置m-1处取得的字符(字节)串 |
len(s) | 字符串s中的字节数 |
len([]rune(s)) | 字符串s中字符的个数,可以使用更快的方法utf8.RuneCountInString() |
[ ]rune(s) | 将字符串s转换为一个unicode值组成的串 |
string(chars) | chars类型是[]rune或者[]int32,将之转换为字符串 |
[ ]byte(s) | 无副本的将字符串s转换为一个原始的字节的切片数组,不保证转换的字节是合法的UTF-8编码字节 |
指针
通常情况下Go语言中的变量持有相应的值。也就是说,我们可以将一个变量想象成它所持有的值来使用。
其中有些例外:通道、函数、方法、map、切片slice 是引用变量,它们持有的都是引用,也即保存指针的变量。
在go中,值在传递给函数或者方法的时候会被复制一次,对于布尔类型和数值类型来说这so easy,但是对于大型变量却代价很大。而且复制值传参的方式,修改值只是修改了副本,这能保证原始变量不被修改,但也一定程度上增加了修改原始值的麻烦。(和C语言一样的道理,同样go也保留了C语言的指针)
Go语言中有指针,也没什么新鲜东西,同样在每次传递给函数或者方法的只是变量的内存地址,这是非常高效的。而且一个被指针指向的变量可以通过该指针来修改,这就很方便的在函数或者方法中通过指针修改原始变量。
Go语言中的指针操作符也是使用&
和*
操作符。
交换两个变量
func swap1(x,y,p *int) { if *x > *y { *x,*y = *y,*x } *p = *x + *y }
map
类比Java的hashmap,在go中,常用于json解码等。map和Java的哈希表一样,必须申请内存,使用make,否则报错
var a map[string]int32; a["aaaa"] = 111; fmt.Printf("%v \n",a);
执行出错,nil就是null,零值。
panic: assignment to entry in nil map
map和slice不一样,slice可以不make直接用,底层会做处理,但是map不行。
var a map[string]int32; a = make(map[string]int32); a["111"] = 111; fmt.Printf("%v \n",a); // map[111:111]
还是使用简洁写法爽,个人也推荐
a := make(map[string]int32);
流程控制
这个东西几乎每个语言都有,下面看常用的几个
if 条件
package main import ( "fmt" ) func main() { x := 1; if x > 1 { // 注意条件表达式没有括号包住() fmt.Println(x); } else if x < 1 { fmt.Println(x); } else { fmt.Println(x); } }
for 循环语句,两种用法
1、当做普通的for循环
package main import ( "fmt" ) func main() { var x int; sum := 0; for x = 1; x <= 10; x++ { sum += x; } fmt.Println(sum); }
2、当做while循环使用(因为go里没有while关键字)
package main import ( "fmt" ) func main() { var x int = 1; sum := 0; for x <= 100 { sum += x; x++; } fmt.Println(sum); }
注意
当:=
操作符用于多个逗号分隔的变量时,如果该变量已经存在,则只是简单的修改它的值。但是当:=
操作符位于作用域的起始处时,Go语言会创建一个新的变量,不管该变量之前是否存在,如在if或者for语句中。
a,c := 2,5 for a := 7; a < 8; a++ { fmt.Println(a) }
例子先使用:=
声明并赋值了三个变量。for
语句处又一次使用:=
操作符声明了变量a
。而for
语句代表了一个新的作用域,所以:=
在这里新声明创建了一个变量a
,这个变量和之前的变量a
是完全不同的两个变量(内存地址不一样)
switch case
package main import ( "fmt" ) func main() { var x int = 1; switch x { // 同样没有括号 case 1: fmt.Println(1); // 不需要break,go自身做了优化,默认给加了break语句。但是也可以手动加上。 case 2: fmt.Println(2); default: fmt.Println(3); } }
如果想让switch语句在case执行后不中断,继续执行接下来的流程,可以使用fallthrough(穿透语句)
package main import ( "fmt" ) func main() { var x int = 1; switch x { // 同样没有括号 case 1: fmt.Println(1); // 不需要break,go自身做了优化,默认给加了break语句。但是也可以手动加上。 fallthrough; case 2: fmt.Println(2); default: fmt.Println(3); } } // 打印 1 换行 2
很重要的一个关键字 range
类似Java的迭代器,可以便捷的遍历容器
package main import ( "fmt" ) func main() { x := [5]int{1,5} for i,v := range x { fmt.Println(i,v) // i相当于key,v相当于value,和Java的foreach很像 } // 当然,可以不用关心key,只取value,go提供了一个符号 _,代表丢弃值 for _,v := range x { fmt.Println(v) } /* 1 2 3 4 5 */ // 还可以遍历字符串,默认打印的是ask码 str := "abcdefg" for i,v := range str { fmt.Println(i,v) } /* 0 97 1 98 2 99 3 100 4 101 5 102 6 103 */ // 如果想打印字符串 for _,v := range str { fmt.Printf("%c \n",v) } /* a b c d e f g */ }
另外,"_" 也叫空标示符,它是一个占位符,用于在赋值操作的时候将某个值赋值给空标示符号,从而达到丢弃该值的目的。空标示符不是一个新的变量,因此将它用于:=
操作符号的时候,必须同时为至少另一个值赋值。