近几年来随着Go语言的流行,其在并发和云计算上的超强优势,使得Go近年来的发展非常迅猛,尤其在区块链领域Go已经是作为首推的第一语言了,大多数招聘网站在招区块链开发人员时精通的语言中都是少不了Go的,在可预见的未来Go语言的前景是非常光明的,因此作为相近与互补原则(学一门OOP语言且能补充java在并发及云端的不足,非go莫属了)如果想要深入去了解Go、java和C++之间的特性与区别,请戳这里:【大神为你分析 Go、Java、C 等主流编程语言】。在这几类语言中,Go的学习曲线和开发效率都是最流畅的,因此花个1-2周的时间来学习这门新语言性价比非常高。
一、Golang的安装与使用
由于Golang官网遭遇撞墙,可以选择到Golang中文网下载Golang的安装包下载:https://www.golangtc.com/download或者https://studygolang.com/dl,二选一即可
Windows 下可以使用 .msi 后缀(在下载列表中可以找到该文件,如go1.9.2.windows-amd64.msi)的安装包来安装。默认情况下.msi文件会安装在 c:\Go 目录下。你可以将 c:\Go\bin 目录添加到 PATH 环境变量中。添加后你需要重启命令窗口才能生效。
IDE的编译器我采用的是goland,JetBrains全家桶的一员,亲测可用的License Serve【http://idea.youbbs.org】。
1.Go语言结构和基础语法
go文件中包含的语言结构如下:包声明、引入包、函数、变量、语句 & 表达式、注释
main包:其中package main表示一个可独立执行的程序,每个 Go 应用程序都包含一个名为 main 的包。包是结构化代码的一种方式:每个程序都由包(通常简称为 pkg)的概念组成,可以使用自身的包或者从其它包中导入内容。如果对一个包进行更改或重新编译,所有引用了这个包的客户端程序都必须全部重新编译。
如同其它一些编程语言中的类库或命名空间的概念,每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成
无分号:在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。
多种数字类型:整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。go 1.9版本对于数字类型,无需定义int及float32、float64,系统会自动识别。
func main() {
var a = 1.5
var b =2
fmt.Println(a,b)
}
变量声明:
var a int = 10 //第一种,指定变量类型,声明后若不赋值,使用默认值。
var b = 10 //第二种,根据值自行判定变量类型。
c := 10 //第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的。常用语函数体和代码块中
多变量
var x, y int //类型相同多个变量, 一般非全局变量
var ( // 这种因式分解关键字的写法一般用于声明全局变量
a int
b bool
)
var c, d int = 1, 2
var e, f = 123, "hello" //不需要显示声明类型,自动推断
//这种不带声明格式的只能在函数体中出现
//g, h := 123, "hello"
func main(){
g, h := 123, "hello"
println(x, y, a, b, c, d, e, f, g, h)
}
如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20
就是不被允许的,编译器会提示错误 no new variables on left side of :=
,但是 a = 20
是可以的,因为这是给相同的变量赋予一个新的值。
类型转换
类型 B 的值 = 类型 B(类型 A 的值),具有相同底层类型的变量之间可以相互转换,当从一个取值范围较大的转换到取值范围较小的类型时就会报错。
a := 5.0
b := int(a)
常量
func main() {
const LENGTH int = 10 //显式类型定义
const WIDTH int = 5
var area int
const a, b, c = 1, false, "str" //多重赋值
area = LENGTH * WIDTH
fmt.Printf("面积为 : %d", area)
println()
println(a, b, c)
}
常量做枚举
const (
Unknown = 0
Female = 1
Male = 2
)
iota,特殊常量
可以认为是一个可以被编译器修改的常量。在每一个const关键字出现时,被重置为0,然后再下一个const出现之前,每出现一次iota,其所代表的数字会自动增加1。iota 可以被用作枚举值:
func main() {
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
fmt.Println(a,b,c,d,e,f,g,h,i)
运算符
&
返回变量存储地址,&a
将给出变量的实际地址。
*
指针变量,*a
是一个指针变量。
位运算符
- 二元运算符,按位与 &:
1 & 0 -> 0
,按位或 |:0 | 1 -> 1
- 一元运算符,
位左移 <<:即 2 的 n 次方
1 << 10 // 等于 1 KB
1 << 20 // 等于 1 MB
1 << 30 // 等于 1 GB
type ByteSize float64
const (
_ = iota // 通过赋值给空白标识符来忽略值
KB ByteSize = 1<<(10*iota)
MB
GB
TB
PB
EB
ZB
YB
)
在通讯中使用位左移表示标识的用例
type BitFlag int
const (
Active BitFlag = 1 << iota // 1 << 0 == 1
Send // 1 << 1 == 2
Receive // 1 << 2 == 4
)
flag := Active | Send // == 3
- 位右移>>:结果是当前值除以 2 的 n 次方。
当希望把结果赋值给第一个操作数时,可以简写为a <<= 2
或者b ^= a & 0xffffffff
。
字符串
作为一种基本数据结构,每种语言都有一些对于字符串的预定义处理函数。Go 中使用 strings
包来完成对字符串的主要操作。
//字符串前缀
strings.HasPrefix(s, prefix string) bool
//后缀
strings.HasSuffix(s, suffix string) bool
//字符串包含
strings.Contains(s, substr string) bool
//字符串出现位置
strings.Index(s, str string) int
//替换
strings.Replace(str, old, new, n) string
//统计次数
strings.Count(s, str string) int
在格式化字符串里,%d
用于格式化整数(%x
和 %X
用于格式化 16 进制表示的数字),%g
用于格式化浮点型(%f
输出浮点数,%e
输出科学计数表示法),%0d
用于规定输出定长的整数,其中开头的数字 0 是必须的。
2.条件语句与函数
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}
Type Switch
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。Type Switch 语法格式如下:
func main() {
var x interface{}
switch i := x.(type) {
case nil:
fmt.Printf(" x 的类型 :%T",i)
case int:
fmt.Printf("x 是 int 型")
case float64:
fmt.Printf("x 是 float64 型")
case func(int) float64:
fmt.Printf("x 是 func(int) 型")
case bool, string:
fmt.Printf("x 是 bool 或 string 型" )
default:
fmt.Printf("未知型")
}
}
go语言中的 switch case
表达式也支持运算
switch result := calculate(); {
case result < 0:
...
case result > 0:
...
default:
// 0
}
select是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。
select随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。
func main() {
var c1, c2, c3 chan int
var i1, i2 int
select {
case i1 = <-c1:
fmt.Printf("received ", i1, " from c1\n")
case c2 <- i2:
fmt.Printf("sent ", i2, " to c2\n")
case i3, ok := (<-c3): // same as: i3, ok := <-c3
if ok {
fmt.Printf("received ", i3, " from c3\n")
} else {
fmt.Printf("c3 is closed\n")
}
default:
fmt.Printf("no communication\n")
}
}
函数
func functionName(parameter_list) (return_value_list) {
…
}
- parameter_list 的形式为 (param1 type1, param2 type2, …)
- return_value_list 的形式为 (ret1 type1, ret2 type2, …)
Go 函数可以返回多个值,例如:
//函数返回多个值
func swap(var1, var2 string) (string, string){
return var2,var1
}
init
是一类非常特殊的函数,它不能够被人为调用,而是在每个包完成初始化后自动执行,并且执行优先级比 main 函数高。每一个源文件都可以包含一个或多个 init 函数。初始化总是以单线程执行,并且按照包的依赖关系顺序执行。
多值返回的四种习惯性写法:
value, err := pack1.Function1(param1)
if err != nil {
fmt.Printf("An error occured in pack1.Function1 with parameter %v", param1)
return err
}
// 未发生错误,继续执行:
if err != nil {
fmt.Printf("Program stopping with error %v", err)
os.Exit(1)
}
if err := file.Chmod(0664); err != nil {
fmt.Println(err)
return err
}
if value, ok := readData(); ok {
…
}
值传递、引用传递
传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。(指针的格式化标识符为 %p
)
引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。引用传递指针参数传递到函数内,以下是交换函数 swap() 使用了引用传递:
&
返回变量存储地址,&a
将给出变量的实际地址。
*
指针变量,*a
是一个指针变量。
/* 定义交换值函数*/
func swap(x *int, y *int) {
var temp int
temp = *x /* 保持 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
Demo案例:
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200
fmt.Printf("交换前,a 的值 : %d\n", a )
fmt.Printf("交换前,b 的值 : %d\n", b )
/* 调用 swap() 函数
* &a 指向 a 指针,a 变量的地址
* &b 指向 b 指针,b 变量的地址
*/
swap(&a, &b)
fmt.Printf("交换后,a 的值 : %d\n", a )
fmt.Printf("交换后,b 的值 : %d\n", b )
}
func swap(x *int, y *int) {
var temp int
temp = *x /* 保存 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
上面为引用传递,以下为值传递:
func main() {
/* 定义局部变量 */
var a int = 100
var b int = 200
fmt.Printf("交换前 a 的值为 : %d\n", a )
fmt.Printf("交换前 b 的值为 : %d\n", b )
/* 通过调用函数来交换值 */
swap(a, b)
fmt.Printf("交换后 a 的值 : %d\n", a )
fmt.Printf("交换后 b 的值 : %d\n", b )
}
/* 定义相互交换值的函数 */
func swap(x, y int) int {
var temp int
temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/
return temp;
}
3.数组
func main(){
//数组的声明
var a [5] float32
fmt.Println(a)
//数组的初始化
var b = [...]float32{1,2,3}
fmt.Println(b[2])
}
简单的一个数组循环赋值和显示Demo:
func main() {
var n [10]int /* n 是一个长度为 10 的数组 */
var i,j int
/* 为数组 n 初始化元素 */
for i = 0; i < 10; i++ { //go中的for循环不需要()
n[i] = i + 100 /* 设置元素为 i + 100 */
}
/* 输出每个数组元素的值 */
for j = 0; j < 10; j++ {
fmt.Printf("Element[%d] = %d\n", j, n[j] )
}
}
实例中函数接收整型数组参数,另一个参数指定了数组元素的个数,并返回平均值:
func main() {
/* 数组长度为 5 */
var balance = []int {1000, 2, 3, 17, 50}
var avg float32
/* 数组作为参数传递给函数 */
avg = getAverage( balance, 5 ) ;
/* 输出返回的平均值 */
fmt.Printf( "平均值为: %f ", avg );
}
func getAverage(arr []int, size int) float32 {
var i,sum int
var avg float32
for i = 0; i < size;i++ {
sum += arr[i]
}
avg = float32(sum) / float32(size)
return avg;
}
4.Go语言的指针
Go 语言的取地址符是&
,放到一个变量前使用就会返回相应变量的内存地址。一个指针变量可以指向任何一个值的内存地址。比如取一个int类型的地址:
func main() {
var a int = 10
fmt.Printf("变量的地址: %x\n", &a ) //printf针对不同的变量format不同
}
一个指针变量可以指向任何一个值的内存地址它指向那个值的内存地址。类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
声明了整型指针数组
const MAX int = 3
func main() {
a := []int{10,100,200}
var i int
var ptr [MAX]*int;
for i = 0; i < MAX; i++ {
ptr[i] = &a[i] /* 整数地址赋值给指针数组 */
}
for i = 0; i < MAX; i++ {
fmt.Printf("a[%d] = %d\n", i,*ptr[i] )
}
}
指向指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址:

声明格式如下:
var ptr **int;
Demo案例
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
/* 指针 ptr 地址 */
ptr = &a
/* 指向指针 ptr 地址 */
pptr = &ptr
/* 获取 pptr 的值 */
fmt.Printf("变量 a = %d\n", a )
fmt.Printf("指针变量 *ptr = %d\n", *ptr )
fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr)
}
结果如下:
变量 a = 3000
指针变量 *ptr = 3000
指向指针的指针变量 **pptr = 3000
Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可。
func main() {
var a int = 100;
var b int = 200;
fmt.Printf("a的值为%d\n",a)
fmt.Printf("b的值为%d\n",b)
swap(&a,&b)
fmt.Printf("a的值为%d\n",a)
fmt.Printf("b的值为%d\n",b)
}
//用指针变量进行参数传递,来替换值,a、b的值发生了变化
func swap(x *int , y *int){
var temp int
temp=*x
*x=*y
*y=temp
}
注意事项:不能得到一个文字或常量的地址,例如const声明的
指针的一个高级应用是你可以传递一个变量的引用(如函数的参数),这样不会传递变量的拷贝。指针传递是很廉价的,只占用 4 个或 8 个字节。当程序在工作中需要占用大量的内存,或很多变量,或者两者都有,使用指针会减少内存占用和提高效率。被指向的变量也保存在内存中,直到没有任何指针指向它们,所以从它们被创建开始就具有相互独立的生命周期。
5.Go的结构体
结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体有中有一个或多个成员。type 语句设定了结构体的名称。可以像其他数据类型一样将结构体类型作为参数传递给函数。并以以上实例的方式访问结构体变量:
type Books struct {
title string
author string
subject string
book_id int
}
func printBook( book Books ) {
fmt.Printf( "Book title : %s\n", book.title);
fmt.Printf( "Book author : %s\n", book.author);
fmt.Printf( "Book subject : %s\n", book.subject);
fmt.Printf( "Book book_id : %d\n", book.book_id);
}
func main() {
var Book1 Books /* 声明 Book1 为 Books 类型 */
/* book 1 描述 */
Book1.title = "Go 语言"
Book1.author = "www.runoob.com"
Book1.subject = "Go 语言教程"
Book1.book_id = 6495407
/* 打印 Book1 信息 */
printBook(Book1)
}
6.Slice切片
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。类比java语言,可以将切片理解为ArrayList
(可动态扩容的增强数组)
切片定义的三种Demo:
//直接声明
var identifier []type
//make函数创建
var slice1 []type = make([]type, len)
// :=式声明
slice1 := make([]type, len)
// 将s中起始索引到结束索引的数组元素赋值给s1,注意包头不包尾!!!
s1 := s[startIndex:endIndex]
切片是可索引的,并且可以由len()
方法获取长度。
切片提供了计算容量的方法cap()
可以测量切片最长可以达到多少。
切片截取:s1 := s[startIndex:endIndex]
,包头不包尾
append() 和 copy() 函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法。
sl_from := []int{1, 2, 3}
sl_to := make([]int, 10)
//n - 拷贝的元素个数
n := copy(sl_to, sl_from)
fmt.Println(sl_to)
fmt.Printf("Copied %d elements\n", n) // n == 3
sl3 := []int{1, 2, 3}
sl3 = append(sl3, 4, 5, 6)
fmt.Println(sl3)
func main() {
var numbers []int
printSlice(numbers)
/* 同时添加多个元素 */
numbers = append(numbers, 2,3,4)
printSlice(numbers)
/* 创建切片 numbers1 是之前切片的两倍容量*/
numbers1 := make([]int, len(numbers), (cap(numbers))*2)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)
printSlice(numbers1)
}
func printSlice(x []int){
fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}
new()
和make()
的区别
new(T)
为每个新的类型T
分配一片内存,初始化为 0 并且返回类型为*T
的内存地址:这种方法 返回一个指向类型为T
,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于&T{}
。make(T)
返回一个类型为T
的初始值,它只适用于3种内建的引用类型:切片、map 和 channel。
切片重组
我们的切片在达到容量上限后可以扩容。改变切片长度的过程称之为切片重组 reslicing,做法如下:slice1 = slice1[0:end]
,其中 end 是新的末尾索引(即长度)。
sl = sl[0:len(sl)+1]
切片的复制与追加
7.Range范围
Go 语言中range
关键字用于for循环中迭代数组(array)
、切片(slice)
、通道(channel)
或集合(map)
的元素。在数组和切片中它返回元素的索引值,在集合中返回key-value
对的key
值。这个range的作用如同java中对应的Iterator迭代器。
//这是我们使用range去求一个slice的和。使用数组跟这个很类似
nums := []int{2, 3, 4}
sum := 0
for _, num := range nums {
sum += num
}
//在数组上使用range将传入index和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。
for i, num := range nums {
if num == 3 {
fmt.Println("index:", i)
}
}
//range也可以用在map的键值对上。
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
fmt.Printf("%s -> %s\n", k, v)
}
//range也可以用来枚举Unicode字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
8.Map
Map 是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的。
/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type
/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
map 是引用类型的: 内存用 make
方法来分配。不要使用 new,永远用 make 来构造 map。注意:如果你错误的使用 new()
分配了一个引用对象,你会获得一个空引用的指针,相当于声明了一个未初始化的变量并且取了它的地址,创建一个 nil map
,而 nil map
不能用来存放键值对。
map 的初始化:
var map1[keytype]valuetype = make(map[keytype]valuetype)
func main() {
//声明创建方式
//var countryCapitalMap = map[string]string{"France":"Paris","Italy":"Rome"}
/* make创建集合 */
countryCapitalMap = make(map[string]string)
/* map 插入 key-value 对,各个国家对应的首都 */
countryCapitalMap["France"] = "Paris"
countryCapitalMap["Italy"] = "Rome"
countryCapitalMap["Japan"] = "Tokyo"
countryCapitalMap["India"] = "New Delhi"
/* 使用 key 输出 map 值 */
for country := range countryCapitalMap {
fmt.Println("Capital of",country,"is",countryCapitalMap[country])
}
/* 查看元素在集合中是否存在 */
captial, ok := countryCapitalMap["United States"]
/* 如果 ok 是 true, 则存在,否则不存在 */
if(ok){
fmt.Println("Capital of United States is", captial)
}else {
fmt.Println("Capital of United States is not present")
}
}
map判空
可以如此处理:val1, isPresent = map1[key1]
isPresent
返回一个 bool
值:如果 key1 存在于 map1,val1 就是 key1 对应的 value 值,并且 isPresent
为 true
;如果 key1 不存在,val1 就是一个空值,并且 isPresent
会返回 false
。
如果只是想判断某个 key 是否存在而不关心它对应的值到底是多少,可以这么做:
// 如果key1存在则ok == true,否则ok为false
_, ok := map1[key1]
// 和if混用
if _, ok := map1[key1]; ok {
// ...
}
9.结构体与方法
结构体定义方式一般为:
type identifier struct {
field1 type1
field2 type2
...
}
使用 new 函数给一个新的结构体变量分配内存,它返回指向已分配内存的指针:var t *T = new(T)
。var t T
也会给 t
分配内存,并零值化内存,但是这个时候 t
是类型 T
。
无论变量是一个结构体类型还是一个结构体类型指针,都使用同样的选择器符(selector-notation) 来引用结构体的字段:
type myStruct struct { i int }
var v myStruct // v是结构体类型变量
var p *myStruct // p是指向一个结构体类型变量的指针
v.i
p.i
结构体工厂
Go 语言不支持面向对象编程语言中那样的构造子方法,但是可以很容易的在 Go 中实现 “构造子工厂”方法。为了方便通常会为类型定义一个工厂,按惯例,工厂的名字以 new 或 New 开头。
type File struct {
fd int // 文件描述符
name string // 文件名
}
结构体类型对应的工厂方法,它返回一个指向结构体实例的指针:
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
return &File{fd, name}
}
调用方式:f := NewFile(10, "./test.txt")
强制使用工厂方法,可以禁止使用 new 函数,强制用户使用工厂方法,从而使类型变成私有的,就像在面向对象语言中那样。
type matrix struct {
...
}
func NewMatrix(params) *matrix {
m := new(matrix) // 初始化 m
return m
}
wrong := new(matrix.matrix) // 编译失败(matrix 是私有的)
right := matrix.NewMatrix(...) // 实例化 matrix 的唯一方式
10.接口
接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。
type Namer interface {
Method1(param_list) return_type
Method2(param_list) return_type
...
}
go约定接口的名字由方法名加 [e]r 后缀组成,例如 Printer、Reader等等,或接口名以 able 结尾。Go 语言中的接口都很简短,通常它们会包含 0 个、最多 3 个方法。
- 类型不需要显式声明它实现了某个接口:接口被隐式地实现。多个类型可以实现同一个接口。
- 实现某个接口的类型(除了实现接口方法外)可以有其他的方法。
- 一个类型可以实现多个接口。
- 接口类型可以包含一个实例的引用, 该实例的类型实现了此接口(接口是动态类型)。
学习笔记来源于《Go入门指南》
网友评论