Go语言学习笔记(一)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jxm007love/article/details/79980647

Go语言学习笔记(一)

本博客是为了记录在学习Go语言过程中应该需要特别记忆的知识点,以防后续需要查看时可以到博客中查找之前的学习记录。本系列博客内容都来自于人民邮电出版社出版由许式伟、吕桂华等编著的《Go语言编程》一书。

  • 语言特性
  • 数据类型
  • 流程控制
  • 函数
  • 错误处理
  • 类型系统
  • 类型的组合、可见性
  • 接口
  • 网络编程
  • JSON处理

语言特性

1、自动垃圾回收
写过C++程序的兄弟们都知道需要手动进行相关资源的内存释放,否则会引起内存泄露问题,但在某些情况下,即使是编写了主动释放内存的代码,也可能会因为各种各样的原因导致释放内存的程序没有被调用,最终可能导致系统崩溃。手动管理内存的另外一个问题就是由于指针的到处传递而无法确定何时可以释放指针所指向的内存块,当然这个问题并不能掩盖C++的很多其他优秀特性。
Go语言作为一门新生的语言,当然不能忽略内存管理这个问题。因为GO语言没有C++这么“强大”的指针计算功能,因此可以很自然的包含垃圾回收功能。因为垃圾回收功能的支持,开发者无需担心所指向的对象失效的问题,因此Go语言中不需要delete关键字,也不需要free()方法来明确释放内存。
2、更丰富的内置类型
除了几乎所有语言都支持的简单内置类型(比如整型和浮点型等)外,Go语言也内置了一些比较新的语言中内置的高级类型,比如C#和Java中的数据和字符串。除此之外,Go还内置了一个对于其他静态类型语言通常用库方式支持的字典类型(map)。
还有一个新增的数据类型:数组切片(Slice)。我们可以认为数组切片是一种可动态增长的数组。
3、函数多返回值(这个特性是我认为Go语言最大的特点)
目前的主流语言中除Python外基本都不支持函数的多返回值功能。在Java中如果想要返回多个返回值,就需要为这些返回值单独定一个对象进行封装,否则无法返回。
Go语言革命性的在静态开发语言阵营中率先提供了多返回值功能。这个特性让开发者可以从原来用各种比较别扭的方式返回多个值的痛苦汇中解脱出来。

func getName(firstName,middleName,lastName,nickName string){
     return "may","m","Chen","Babe"
}

并不是每一个返回值都必须赋值,没有被明确赋值的返回值将保持默认的空值。可用如下方式进行调用:

fn,mn,ln,nn := getName()

如果开发者只对该函数其中几个返回值感兴趣的话,也可以直接用下划线作为占位符来忽略其他不关心的返回值。例如:

_,_,lastname,_ := getName()

可以只接收lastname的值。
4、错误处理
Go语言引入了defer关键字用于标准的错误处理流程,并提供了内置函数panic、recovery完成异常的抛出与捕获。
5、匿名函数和闭包
在Go语言中,所有的函数也是值类型,可以作为参数传递。Go语言支持常规的匿名函数和闭包。
6、类型和接口
Go语言中的类型定义非常接近于C语言中的结构体(struct),甚至沿用了struct关键字。相比而言,Go语言并没有直接沿用C++和Java的传统去设计一个超级复杂的类型系统,不支持继承和重载,而只是支持最基本的类型组合功能。
Go语言也不是简单的对面向对象开发语言做减法,它还引入了一个无比强大的“非侵入式”接口的概念,即在实现一个接口前不需要事先定义接口,不需要将类型与接口紧密绑定

type Bird struct {
     //鸟的相关定义
}
func (b *Bird) Fly(){
}
 type IFly interface{
    Fly()
}

类型Bird与接口IFly看起来没有任何关系,下面的语句可以使用它们。

   func main(){
        var fly IFly = new(Bird)
        fly.Fly
   }

可以看出虽然Bird类型实现的时候,没有声明与接口IFly的关系,但接口可以和类型直接转换。
7、并发编程
Go语言引入了goroutine概念,通过使用goroutine而不是裸用操作系统的并发机制,以及使用消息传递来共享内存而不是使用共享内存来通信,Go语言让并发编程变得更加轻盈和安全。
8、反射
反射是在Java语言出现后迅速流行起来的一种概念。通过反射,可以获取对象类型的详细信息,并可动态操作对象。
Go语言的反射实现了反射的大部分功能,但没有像Java语言那样内置类型工厂,故而无法做到像Java那样通过类型字符串创建对象实例。反射最常见的使用场景是序列化和反序列化。例如Go语言的标准库encoding/json、encoding/xml、encoding/gob、encoding/binary等包就大量依赖于反射功能来实现。

数据类型

变量
变量是几乎所有编程语言中最基本的组成元素。
变量声明:

var v1 int
var v2 string
var v3 [10]int //数组
var v4 []int //数组切片
var v5 struct{
f int
}
var v6 *int //指针
var v7 map[string]int //key为string类型,value为int类型
var v8 func(a int) int

变量声明语句不需要使用分号作为结束符。

变量初始化:
对于声明变量时需要初始化的场景,var关键字可以保留,但不再是必要的元素,如下所示:

var v1 int =10
var v2 = 10
v3 := 10

变量赋值:
在Go语法中,变量初始化和变量赋值是两个不同的概念。下面为声明一个变量之后的赋值过程:

var v10 int
v10 = 123

Go语言还提供了多重赋值功能,比如交换i和j变量的语句:

i,j = j,i

如果不支持多重赋值,则实现这一功能需要3条语句,写过java冒泡排序程序的肯定对交换数据有着深刻的记忆。
匿名变量:

_,_,nickname := GetName()

这种用法可以让代码非常简洁,只保留需要的变量即可,基本上屏蔽掉了可能混淆代码阅读者的内容。


常量
字面常量:
所谓字面常量,是指程序中硬编码的常量,如:

-12
3.14159265358979323846 //浮点类型常量
3.2+12i //复数类型常量
true //布尔类型常量
"foo" //字符串常量

在其他语言中,常量通常有特定的类型,比如-12在C语言中会被认为是一个int类型的常量。如果要指定-12为一个long类型的常量,需要写成-12L。Go语言中的字面常量更接近自然语言中的常量,是无类型的。只要这个常量在相应类型的值域范围内,就可以作为这个类型的常量,比如-12可以被赋值给int、uint、int32、int64、float32、float64、complex64、complex128等类型的变量。
常量定义:
通过const关键字,你可以给字面常量定义一个友好的名字:

const Pi float64 = 3.14159265358979323846
const zero = 0.0
const (
size int64 = 1024
eof = -1
)
const u,v float32 = 0,3
const a,b,c = 3,4,"foo"

由于常量赋值是一个编译期行为,所以右值不能出现任何需要运行期才能得出结果的表达式。
预定义常量:
Go语言预定义了这些常量:true、false、iota
iota比较特殊,可以被认为是一个可被编译器修改的常量,在每一个const关键字出现时被重置为0,在下一个const出现之前,每出现一次iota,其所代表的数字会自动增1。
例如:

const ( //iota被重置为0
c0 = iota //c0=0
c1 = iota //c1=1
c2 = iota //c2=2
)
const (
a = 1<<iota //a=1
b = 1<<iota //b=2
c = 1<<iota //c=4
)
const (
u = iota * 42 //u=0
v float64 = iota *42 //v=42
w = iota *42 //w=84
)

枚举:
枚举指一系列相关的常量,比如下面关于一个星期中每天的含义。可以用const后面跟一对圆括号的方式定义一组常量,这种定义法在Go语言中通常用于定义枚举值。Go语言并不支持其他语言支持的enum关键字。

const (
Sunday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)

同其他的符号一样,以大写字母开头的常量在包外可见。


类型
Go语言内置以下基础类型:

  • 布尔类型:bool

  • 整型:int8、byte、int16、int、uint、uintptr等

  • 浮点型:float32、float64

  • 复数类型:complex64、complex128

  • 字符串:string

  • 字符类型:rune

  • 错误类型:error

    此外Go语言还支持以下复合类型:
    指针(pointer)
    数组(array)
    切片(slice)
    字典(map)
    通道(chan)
    结构体(struct)
    接口(interface)
    对于常规开发,用int和uint即可,没必要指定int8之类的类型,以避免一致困难。
    布尔类型:
    Go语言中的布尔类型与其他语言基本一致,关键字也为bool,可赋值为预定义的true和false,示例代码如下:

    var v1 bool
    v1 = true
    v2 := (1==2)

    布尔类型不能接受其他类型的赋值,不支持自动或强制的类型转换。
    整型:
    整型是所有编程语言里最基础的数据类型。Go语言支持如下表所示的整型类型:

    类型 长度(字节) 值范围
    int8 1 -128~127
    uint8 1 0~255
    int16 2 -32768~32767
    uint16 2 0~65535
    int32 4 -2147483648~2147483647
    uint32 4 0~4294967295
    int64 8 -9223372036854775808~9223372036854775807
    uint64 8 0~18446744073709551615
    int 平台相关 平台相关
    uint 平台相关 平台相关
    uintptr 同指针 在32位平台下为4字节,在64位平台下为8字节

    需要注意的是,int和int32在Go语言中是两种不同类型,编译器也不会自动帮你做类型转换。直接赋值会产生编译错误,使用强制类型转换可以完成不同类型的赋值,但需要注意在强制类型转换过程中产生的精度缺失和值溢出,例如:

    var value2 int32
    value1 := 64
    value2 = int32(value1)

    Go语言支持:+、-、*、/和%5种数值运算。
    Go语言支持:>、<、==、>=、<=和!=这几种比较运算符。
    浮点型:
    浮点型用于表示包含小数点的数据,比如1.234就是一个浮点型数据。Go语言中的浮点型采用IEEE-754标准的表达方式。
    浮点数表示:
    Go语言定义了两个类型float32和float64,其中float32等价于C语言的float类型,float64等价于C语言的double类型。
    浮点数比较:
    因为浮点数不是一种精确的表达方式,所以不能用==直接来判断两个浮点数是否相等,如下方法可以作为替代方案:

    import "math"
    func IsEqual(f1,f2,p float64) bool{
    return math.Abs(f1-f2) < p
    }


字符串:
在Go语言中,字符串也是一种基本类型。声明和初始化一个字符串非常简单,举例如下:

var str string
str = "Hello World"
ch := str[0] //取字符串第一个字符

字符串操作:x+y表示字符串连接;len(s)表示求字符串长度;s[i]取指定位置的字符。更多的字符串操作,请参考标准库string包。
字符串遍历:
Go语言支持两种方式遍历字符串,一种是以字节数组的方式进行遍历:

str := "Hello World"
n := len(str)
for i := 0;i<n;i++{
ch := str[i] //依据下标取字符串中的字符,类型为byte
fmt.Printf(i,ch)
}

另一种是Unicode字符遍历:

str := "Hello World"
for i,ch := range str{
fmt.Printf(i,ch) //ch类型为rune
}

字符类型:
在Go语言中支持两种字符类型,一个是byte(实际上是uint8的别名),代表UTF-8字符串的单个字节的值;另一个是rune,代表单个Unicode字符。


数组:
数组是指一系列同一类型数据的集合。以下为一些常见的数据声明方法:

[32]byte
[2*N]struct{x,y int32} //复杂类型数组
[1000]*float64 //指针数组
[3][5]int //二维数组
[2][2][2]float64 //三维数组

元素访问:可以使用数组下标来访问数组元素。Go语言还提供了一个关键字range,用于便捷地遍历容器中的元素,数组也是range的支持范围。例如:

for i,v := range array{
fmt.Printf("Array element[",i,"]=",v)
}

Go语言中数组是一个值类型,所有的值类型变量在赋值和作为参数传递时都将产生一次复制动作。


数组切片:
由于数组具有以下特点:在定义之后长度就无法再进行修改;数组是值类型,每次传递都将产生一个副本。在实际开发中无法满足开发者的真实需求,Go语言提供了数组切片功能来弥补数组的不足。数组切片的数据结构可以抽象为以下3个变量:
1. 一个指向原生数组的指针
2. 数组切片中元素的个数
3. 数组切片已分配的存储空间
基于数组,数组切片添加了一系列管理功能,可以随时动态扩展存放空间,并且可以随意传递而不会导致所管理的元素被重复复制。
创建数组切片的方法有两种:基于数组和直接创建。
数组切片可以基于一个已存在的数组创建。数组切片可以使用数组的一部分元素或整个数组来创建。如下所示:

var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10}
var mySlice []int = myArray[:5] //表示用数组的前5个元素创建一个切片
var mySlice1 []int = myArray[:] //用数组全部元素创建一个切片
var mySlice2 []int = myArray[5:] //从第5个元素之后所有元素创建一个切片

并非要事先有一个数组才能够创建一个切片,Go语言提供了make()函数可以灵活的创建切片。

mySlice3 := make([]int,5) //创建一个初始元素个数为5,元素初始值为0的数组切片
mySlice4 := make([]int,5,10) //创建一个元素个数为5的数组切片,元素初始值为0,并预留10个元素的存储空间
mySlice5 := []int{1,2,3,4,5} //直接创建并初始化包含5个元素的数组切片

操作数据的方法都适用于数组切片,比如数组切片也可以按下标读写元素,用len()函数获取元素个数,并支持使用range关键字来遍历所有的元素。并支持使用range关键字来快速遍历所有元素。
传统的元素遍历方法:

for i:=0;i<len(myslice);i++{
   fmt.Println("mySlice[",i,"]=",mySlice[i])
}

使用range关键字可以让代码显得更加整洁,有两个返回值,第一个是索引,第二个是元素的值:

for i,v:=range mySlice{
   fmt.Println("mySlice[",i,"]=",v)
}

可动态增减元素是数组切片比数组更为强大的功能。与数组相比,数组切片多了一个存储能力(capacity)的概念,即元素个数和分配的空间可以使两个不同的值。合理设置存储能力的值,可以大幅降低数组切片内部重新分配内存和搬运内存块的频率,从而大大提高程序的性能。
数组切片支持Go语言内置的cap()函数和len()函数,cap()函数返回的是数组切片分配的空间大小,而len()函数返回的是数组切片中当前所存储的元素个数。
如果需要向已有的切片中继续新增元素,可以使用append()函数,从尾端给mySlice加上3个元素,从而生成一个新的切片。

mySlice = append(mySlice,1,2,3)

函数append()的第二个参数其实是一个不定参数,我们可以按自己需求添加若干个元素,甚至直接将一个数组切片追加到另一个数组切片的末尾:

mySlice2 := []int(8,9,10)
mySlice = append(mySlice,mySlice2...)

注意第二个参数mySlice2后面加了3个点,即一个省略号,如果没有这个省略号的话,会有编译错误,因为按照append()的语义,从第二个参数起所有的参数都是待附加的元素。因为mySlice中的元素类型为int,所以直接传mySlice2是行不通的,加上省略号相当于把mySlice2包含的所有元素打散后传入。数组切片会自动处理存储空间不足的问题。
数组切片也可以基于另一个数组切片创建。

oldSlice := []int{1,2,3,4,5}
newSlice := oldSlice[:3] //基于oldSlice的前3个元素构建新的数组切片

选择的oldSlice元素范围可以超过所包含的元素,比如newSlice可以基于oldSlice的前6个元素创建,虽然oldSlice只有5个元素,只要这个选择范围不超过oldSlice的储存能力(即cap()返回的值),那么这个创建程序就是合法的,newSlice中超出oldSlice元素的部分都会填上0。
数组切片支持Go语言的另一个内置函数copy(),用于将内容从一个数组切片复制到另一个数组切片。如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。如下例所示:

slice1 := []int{1,2,3,4,5}
slice2 := []int{8,9,10}
copy(slice2,slice1)  //只会复制slice1中前3个元素到slice2中
copy(slice1,slice2)  //只会复制slice2的3个元素到slice1的前3个元素

在Go语言中使用map不需要引入任何库,并且用起来也更加方便。map是一堆键值对的未排序集合。
变量声明:

var myMap map[string] PersonInfo

其中myMap是声明的变量名,string是键的类型,PersonInfo是其中存放的值类型。
创建:
可以使用Go语言内置的函数make()来创建一个新的map。

myMap = make(map[string] PersonInfo,100)

元素赋值:

myMap["1234"] = PersonInfo{"1","jack","Room 1001..."}

元素删除:
Go语言提供了一个内置函数delete(),用于删除容器内的元素。

delete(myMap,"1234")

上述代码将从myMap中删除键为”1234”的键值对。如果”1234”这个键不存在,那么这个调用将什么都不发生,也不会有什么副作用。但是如果传入的map变量的值是nil,该调用将导致程序抛出异常。
元素查找:
在Go语言中查找特定的键对应的值,可以用下面的代码实现:

value,ok := myMap["1234"]
if ok {  //表示已经找到对应的value
   //处理找到的value
}

流程控制

程序设计语言的流程控制语句,用于设定计算执行的次序,建立程序的逻辑结构.可以说流程控制语句是整个程序的框架.
Go语言支持如下几种流程控制语句:
1.条件语句:对应的关键字为if,else和else if;
2.选择语句:对应的关键字为switch,case和select
3.循环语句:对应的关键字为for和range
4.跳转语句:对应的关键字为goto
条件语句:
关于条件语句的样例代码如下:

if a < 5 {
  return 0
}else {
  return 1
}  

需要注意的是条件语句中不需要用小括号将判断条件包裹起来;
无论语句体内有几条语句,花括号都是必须存在的;
左花括号{必须与if或else处于同一行;
在if之后,判断条件之前,可以添加变量初始化语句,使用;间隔
在有返回值的函数中,不允许将”最终的”return语句包含在if…else…结构中,否则会编译失败:
function ends without a return statement
失败的原因在于,Go编译器无法找到终止该函数的return语句.编译失败的案例如下:

func example(x int) int {
   if x==0 {
      return 5
   }else{
      return x
   }
}

选择语句:
根据传入条件的不同,选择语句会执行不同的语句.

switch i {
  case 0:
    fmt.Printf("0")
  case 1:
    fmt.Printf("1")
  case 2:
    fallthrough
  case 3:
    fmt.Printf("3")
  case 4,5,6:
    fmt.Printf("4,5,6")
  default:
    fmt.Printf("Default")
}

比较有意思的是,switch后面的表达式甚至不是必须的,比如下面的例子:

switch {
  case 0<= Num && Num <= 3:
    fmt.Printf("0-3")
  case 4<= Num && Num <= 6:
    fmt.Printf("4-6")
  case 7<=Num && Num <= 9:
    fmt.Printf("7-9")
}

在使用switch结构的时候,需要注意以下几点:
1.左花括号{必须与switch处于同一行;
2.条件表达式不限制为常量或整数;
3.单个case中,可以出现多个结果项;
4.与C语言等规则相反,Go语言不需要用break来明确退出一个case;
5.只有在case中明确添加fallthrough关键字,才会继续执行紧跟的下一个case;
6.可以不设定switch之后的条件表达式,在此种情况下,整个switch结构与多个if…else…的逻辑作用等同.
循环语句:

sum := 0
for i := 0;i < 10; i++ {
  sum += i
}

可以看到一个较大的不同在于for后面的条件表达式不需要使用圆括号()包含起来.
使用循环语句时,需要注意的有以下几点:
1.左花括号{必须与for处于同一行
2.G哦语言中的for循环与C语言一样,都允许在循环条件中定义和初始化变量,唯一的区别在于,Go语言不支持以逗号为间隔的多个赋值语句,必须使用平行赋值的方式来初始化多个变量
3.Go语言的for循环同样支持continue和break来控制循环,但是它提供了一个更高级的break,可以选择中断哪一个循环,如下例:

JLoop:
for j:=0;j<5;j++{
  for i:= 0;i<10;i++{
    if i>5 {
       break JLoop
    }
    fmt.Printf(i)
  }
}

本例中,break语句终止的是JLoop标签处的外层循环.
跳转语句:
goto语句被多数语言学者反对,但对于Go语言这样一个惜关键字如金的语言来说,居然仍然支持goto关键字….

函数

函数构成代码执行的逻辑结构。在Go语言中,函数的基本组成为:关键字func、函数名、参数列表、返回值、函数体和返回语句。
函数定义:
  

package mymath
import "errors"

func Add(a int,b int)(ret int,err error){
  if a<0 || b<0 {
    err = errors.New("Should be non-negative numbers!")
    return
  }
  return a+b,nil  //支持多重返回值
}

如果参数列表中若干个相邻的参数类型相同,比如上面的例子中a和b,则可以在参数列表中省略前面的变量的类型声明,如下所示:

func Add(a,b int)(ret int,err errors){
  //......
}

如果返回值列表中多个返回值的类型相同,也可以用同样的方式合并。
如果函数只有一个返回值,可以像下面的方式进行书写:

func Add(a,b int) int {
  //......
}

函数调用:
函数调用非常方便,只要事先导入了该函数所在的包,就可以直接按照如下所示的方式调用函数:

import "mymath"
c := mymath.Add(1,2)

Go语言中的大小写不仅仅是风格,更直接体现了该函数的可见性,这一点尤为需要注意。小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包引用。这个规则也适用于类型和变量的可见性。
不定参数:
Go语言标准库中的fmt.Println()等函数的实现也严重依赖语言的不定参数功能。
不定参数是指函数传入的参数个数为不定数量。为了做到这点,首先需要将函数定义为接受不定参数类型:

func myfunc(args ...int){
  for _,arg : = range args{
    fmt.Println(arg)
  }
}

这段代码的意思是,函数myfunc接受不定数量的参数,这些参数的类型全部是int,所以可用如下方式调用:

myfunc(1,2,3)
myfunc(1,2,3,4)

形如…type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数。
之前的例子是将不定参数类型的约束为int,如果希望传任意类型,可以指定类型为interface{}。下面是Go语言标准库中fmt.Printf()的函数原型:

func Printf(format string,args ...interface{}){
  //....
}

用interface{}传递任意类型数据是Go语言的惯例用法。使用interface{}仍然是类型安全的。
多返回值
与C、C++和Java等开发语言的一个极大的不同在于,Go语言的函数或者成员方法可以有多个返回值,这个特性能够使我们写出比其他语言更优雅、更简洁的代码,比如File.Read()函数就可以同时返回读取的字节数和错误信息。如果读取文件成功,则返回值中的 n为读取字节数,err为nil,否则err为具体的出错信息:

func (file * File)Read (b []byte)(n int,err Error)

从上面的方法原型可以看到,我们还可以给返回值命名,就像函数的输入参数一样。返回值被命名之后,它们的值在函数开始的时候被自动初始化为空。在函数中执行不带任何参数的return语句,会返回对应的返回值变量的类型。
如果调用方调用了一个具有多返回值的方法,但是却不关心其中的某个返回值,可以简单地用一个下划线“_”来跳过这个返回值,比如下面的代码表示调用者在读文件的时候不想关心Read函数返回的错误码:

n,_:=f.Read(buf)

错误处理

error接口
Go语言引入了一个关于错误处理的标准模式,即error接口,该接口的定义如下:

type error interface{
  Error() string
}

对于大多数函数,如果要返回错误,大致上都可以定义为如下模式,将error作为多重返回值中的最后一个,但这并非是强制要求:

func Foo(param int)(n int, err error){
  //......
}

调用时的代码建议按如下方式处理错误情况:

n,err := Foo(0)
if err != nil{
  //错误处理
}
else{
  //使用返回值n
}

类型系统

未完待续

类型的组合和可见性

未完待续

接口

未完待续

网络编程

未完待续

JSON处理

未完待续


阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页