Go语言基础

Go是一门类似C的编译型语言,但是它的编译速度非常快。这门语言的关键字总共也就二十五个,比英文字母还少一个,这对于我们的学习来说就简单了很多。先让我们看一眼这些关键字都长什么样:

下面列举了 Go 代码中会使用到的 25 个关键字或保留字:

  • 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。
  • 大写字母开头的函数也是一样,相当于class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。
  • 1. 变量、常量、Go内置类型

    1.1 变量

    Go语言里面定义变量有多种方式。

    使用 var 关键字是Go最基本的定义变量方式,与C语言不同的是Go把变量类型放在变量名后面:

    //定义一个名称为“valName”,类型为"type"的变量
    var valName type
    

    定义多个变量

    //定义三个类型都是“type”的变量
    var vname1, vname2, vname3 type
    

    定义变量并初始化值

    //初始化“vName”的变量为“value”值,类型是“type”
    var vName type = value
    

    同时初始化多个变量

    定义三个类型都是"type"的变量,并且分别初始化为相应的值 vname1为v1,vname2为v2,vname3为v3 var vname1, vname2, vname3 type= v1, v2, v3

    你是不是觉得上面这样的定义有点繁琐?没关系,因为Go语言的设计者也发现了,有一种写法可以让它变得简单一点。我们可以直接忽略类型声明,那么上面的代码变成这样了:

    定义三个变量,它们分别初始化为相应的值 vname1为v1,vname2为v2,vname3为v3 然后Go会根据其相应值的类型来帮你初始化它们 var vname1, vname2, vname3 = v1, v2, v3

    你觉得上面的还是有些繁琐?好吧,我也觉得。让我们继续简化:

    定义三个变量,它们分别初始化为相应的值 vname1为v1,vname2为v2,vname3为v3 编译器会根据初始化的值自动推导出相应的类型 vname1, vname2, vname3 := v1, v2, v3

    :=这个符号直接取代了vartype,这种形式叫做简短声明。不过它有一个限制,那就是它只能用在函数内部;在函数外部使用则会无法编译通过,所以一般用var方式来定义全局变量。

    _(下划线)是个特殊的变量名,任何赋予它的值都会被丢弃。在这个例子中,我们将值2赋予b,并同时丢弃1

    _, b := 1, 2
    

    Go对于已声明但未使用的变量会在编译阶段报错,比如下面的代码就会产生一个错误:声明了i但未使用:

    package main
    func main() {
        var i int
    

    1.2 常量

    所谓常量,也就是在程序编译阶段就确定下来的值,而程序在运行时无法改变该值。在Go程序中,常量可定义为数值、布尔值或字符串等类型。

    它的语法如下:

    const constantName = value
    //如果需要,也可以明确指定常量的类型:
    const Pi float32 = 3.1415926
    

    下面是一些常量声明的例子:

    const PI = 3.1415926
    const MaxThread = 10
    const Prefix = "so_"
    

    1.3 内置基础类型

    Go 语言按类别有以下几种数据类型:

  • 布尔型 --> 在Go中,布尔值的类型为bool,值只可以是常量 true 或者 false。一个简单的例子:var b bool = true;
  • 整数型 --> 整型 int 和浮点型 float,Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码;
  • 字符串 --> 字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本;
  • 派生型 -->
  • (a) 指针类型(Pointer)
  • (b) 数组类型
  • © 结构化类型(struct)
  • (d) 联合体类型 (union)
  • (e) 函数类型
  • (f ) 切片类型
  • (g) 接口类型(interface)
  • (h) Map 类型
  • (i) Channel 类型
  • Boolean

    //示例代码
    var isActive bool  // 全局变量声明
    var enabled, disabled = true, false  // 忽略类型的声明
    func test() {
        var available bool  // 一般声明
        valid := false      // 简短声明
        available = true    // 赋值操作
    

    整数类型有无符号和带符号两种。Go同时支持intuint,这两种类型的长度相同,但具体长度取决于不同编译器的实现。Go里面也有直接定义好位数的类型:rune, int8, int16, int32, int64byte, uint8, uint16, uint32, uint64。其中runeint32的别称,byteuint8的别称。

    需要注意的一点是,这些类型的变量之间不允许互相赋值或操作,不然会在编译时引起编译器报错。

    如下的代码会产生错误:invalid operation: a + b (mismatched types int8 and int32)

    var a int8

    var b int32

    c:=a + b

    另外,尽管int的长度是32 bit, 但int 与 int32并不可以互用。

    浮点数的类型有float32float64两种(没有float类型),默认是float64

    这就是全部吗?No!Go还支持复数。它的默认类型是complex128(64位实数+64位虚数)。如果需要小一些的,也有complex64(32位实数+32位虚数)。复数的形式为RE + IMi,其中RE是实数部分,IM是虚数部分,而最后的i是虚数单位。下面是一个使用复数的例子:

    var c complex64 = 5+5i
    //output: (5+5i)
    fmt.Printf("Value is: %v", c)
    

    Go中的字符串都是采用UTF-8字符集编码,字符串是用一对双引号("")或反引号()括起来定义,它的类型是 string

    func test(a,b int) {
       str := "hello world"
       m := "haha"
       result := str + m
       println(result)
       multStr := `hello
             world`
       println(multStr)
    

    在Go中字符串是不可变的,例如下面的代码编译时会报错:cannot assign to s[0]

    var s string = "hello"
    s[0] = 'k'
    

    如果你想要修改一个字符串怎么办呢:

    s := "hello"
    c := []byte(s)
    c[0] = 'c'
    s1 := string(c)
    println(s1)
    

    Go中可以使用 + 操作符来连接两个字符串:

    str := "hello world"
    m := "haha"
    result := str + m
    println(result)
    

    如果要声明一个多行的字符串怎么办?可以通过`` 来声明:

    multStr := `hello
             world`go
    println(multStr)
    

    括起的字符串为Raw字符串,即字符串在代码中的形式就是打印时的形式,它没有字符转义,换行也将原样输出。例如本例中会输出:

    hello
        world
    

    Go内置有一个error类型,专门用来处理错误信息,Go的package里面还专门有一个包errors来处理错误:

    func error()  {
       e := errors.New("this is a error demo")
       if e != nil {
          println(e)
    

    1.4 iota枚举

    这个关键字用来声明enum的时候采用,它默认开始值是0,const中每增加一行加1:

    package main
    const (
    	x = iota // x == 0
    const (
    	h, i, j = iota, iota, iota //h=0,i=0,j=0 iota在同一行值相同
    const (
    	l       = iota //a=0
    	m       = "B"
    	n       = iota             //2
    	o, p, q = iota, iota, iota //3,3,3
    	r       = iota             //4
    func main()  {
    	println(x,y,z,w)
    	println(h,i,j)
    	println(l,m,n,o,p,q,r)
    0 1 2 3
    0 0 0
    0 B 2 3 3 3 4
    

    1.5 array,slice,map

    array

    array就是数组,定义方式如下:

    var arr [n]type
    

    [n]type中,n表示数组的长度,type表示存储元素的类型。对数组的操作和其它语言类似,都是通过[]来进行读取或赋值:

    var arr [10]int  // 声明了一个int类型的数组
    arr[0] = 1      // 数组下标是从0开始的
    arr[1] = 2      // 赋值操作
    

    由于长度也是数组类型的一部分,因此[3]int[4]int是不同的类型,数组也就不能改变长度。数组之间的赋值是值的赋值,即当把一个数组作为参数传入函数的时候,传入的其实是该数组的副本,而不是它的指针。如果要使用指针,那么就需要用到后面介绍的slice类型了。

    数组可以使用另一种:=来声明:

    a := [3]int{1, 2, 3} // 声明了一个长度为3的int数组
    b := [10]int{1, 2, 3} // 声明了一个长度为10的int数组,其中前三个元素初始化为1、2、3,其它默认为0
    c := [...]int{4, 5, 6} // 可以省略长度而采用`...`的方式,Go会自动根据元素个数来计算长度
    

    Go支持嵌套数组,即多维数组。比如下面的代码就声明了一个二维数组:

    // 声明了一个二维数组,该数组以两个数组作为元素,其中每个数组中又有4个int类型的元素 doubleArray := [2][4]int{[4]int{1, 2, 3, 4}, [4]int{5, 6, 7, 8}} // 上面的声明可以简化,直接忽略内部的类型 easyArray := [2][4]int{{1, 2, 3, 4}, {5, 6, 7, 8}}

    注意: [2][4] int表示的是一个int型数组,数组内有两个数组,每个数组有四个元素组成。

    slice

    知道python数组的就知道slice,跟python的实现是一样的。

    slice有一些简便的操作

  • slice的默认开始位置是0,ar[: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 aSlice, bSlice []byte
    // 演示一些简便操作
    aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c
    aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
    aSlice = array[:]  // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素
    // 从slice中获取slice
    aSlice = array[3:7]  // aSlice包含元素: d,e,f,g,len=4,cap=7
    bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
    bSlice = aSlice[:3]  // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
    bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h
    bSlice = aSlice[:]   // bSlice包含所有aSlice的元素: d,e,f,g
    

    重要:slice是引用类型,所以当引用改变其中元素的值时,其它的所有引用都会改变该值,例如上面的aSlicebSlice,如果修改了aSlice中元素的值,那么bSlice相对应的值也会改变。

    对于slice有几个有用的内置函数:

  • len 获取slice的长度
  • cap 获取slice的最大容量
  • appendslice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice
  • copy 函数copy从源slicesrc中复制元素到目标dst,并且返回复制的元素的个数
  • //1.基于数组创建数组切片
    var array  = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    var slice = array[1:7] //array[startIndex:endIndex] 不包含endIndex
    //2.直接创建数组切片
    slice2 := make([]int, 5, 10)
    //3.直接创建并初始化数组切片
    slice3 := []int{1, 2, 3, 4, 5, 6}
    //4.基于数组切片创建数组切片
    slice5 := slice3[:4]
    println(slice5)
    //5.遍历数组切片
    for i, v := range slice3 {
    println(i, v)
    //6.len()和cap()
    var len = len(slice2) //数组切片的长度
    var cap = cap(slice)  //数组切片的容量
    println("len(slice2) =", len)
    println("cap(slice) =", cap)
    //7.append() 会生成新的数组切片
    slice4 := append(slice2, 6, 7, 8)
    slice4 = append(slice4, slice3...)
    println(slice4)
    //8.copy() 如果进行操作的两个数组切片元素个数不一致,将会按照个数较小的数组切片进行复制
    copy(slice2, slice3) //将slice3的前五个元素复制给slice2
    println(slice2, slice3)
    

    map就是Java中的Map,python中的字典。它的格式为 map[keyType]valueType

    fruit := map[string]int{"apple":5,"orange":7,"pineapple":3}
    println(fruit)
    var appleCount = fruit["apple"]
    println(appleCount)
    

    使用map过程中需要注意的几点:

  • map是无序的,每次打印出来的map都会不一样,它不能通过index获取,而必须通过key获取
  • map的长度是不固定的,也就是和slice一样,也是一种引用类型
  • 内置的len函数同样适用于map,返回map拥有的key的数量
  • map的值可以很方便的修改,通过numbers["one"]=11可以很容易的把key为one的字典值改为11
  • map和其他基本型别不同,它不是thread-safe,在多个go-routine存取时,必须使用mutex lock机制
  • map的初始化可以通过key:val的方式初始化值,同时map内置有判断是否存在key的方式

    1.6 make,new操作

    make用于内建类型(mapslicechannel)的内存分配。new用于各种类型的内存分配。

    内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指针,指向新分配的类型T的零值。有一点非常重要:

    new返回指针。

    内建函数make(T, args)new(T)有着不同的功能,make只能创建slicemapchannel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须被初始化。例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slicenil。对于slicemapchannel来说,make初始化了内部的数据结构,填充适当的值。

    make返回初始化后的(非零)值。

    对于不同的数据类型,零值的意义是完全不一样的。比如,对于bool类型,零值为false;int的零值为0;string的零值是空字符串:

    b := new(bool)
    println(*b)
    i := new(int)
    println(*i)
    s := new(string)
    println(*s)
    false
    

    注意:上面最后string的输出是空值。

    1.7 零值

    关于“零值”,所指并非是空值,而是一种“变量未填充前”的默认值,通常为0。 此处罗列 部分类型 的 “零值”

    int     0
    int8    0
    int32   0
    int64   0
    uint    0x0
    rune    0 //rune的实际类型是 int32
    byte    0x0 // byte的实际类型是 uint8
    float32 0 //长度为 4 byte
    float64 0 //长度为 8 byte
    bool    false
    string  ""
    

    1.8 一些技巧

    1.分组声明

    在Go语言中,同时声明多个常量、变量,或者导入多个包时,可采用分组的方式进行声明。

    例如下面的代码:

    import	"log"
    import	"net/http"
    import	"strings"
    const a = 3
    const b = 2
    const c = 4
    var str = "aa"
    var prefix = "abc_"
    

    可以写成如下分组形式:

    import (
    	"log"
    	"net/http"
    	"strings"
    const(
    	a = 2,
        b = 2,
        c = 4
    var(
    	str = "aa"
        prefix = "abc_"
    

    Go程序设计的一些规则

    Go之所以会那么简洁,是因为它有一些默认的行为:

  • 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。
  • 大写字母开头的函数也是一样,相当于class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。
  • 2. 流程和函数

    Go中流程控制分三大类:条件判断,循环控制和无条件跳转。

    2.1 流程

    Go里面if条件判断语句中不需要括号,如下代码所示 :

    if x > 80{
        println("better")
    } else {
        println("good")
    

    Go的if还有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下所示

    if countScore := getCountScore(); countScore >= 80 {
        println("better")
    } else if countScore >= 60 {
        println("good")
    } else {
        println("e......")
    //这里打印countScore是找不到这个变量的
    println(countScore)
    

    Go有goto语句——请明智地使用它。用goto跳转到必须在当前函数内定义的标签。例如假设这样一个循环:

    a := 4
    b := 5
    if a > b {
    	println(a * b)
    } else {
    	Ding:
    		a = a+12
    		b = a + (32/4)
    	if a < b {
    		goto Ding
    

    注意:标签名是大小写敏感的

    Go里面的for除了基本的循环外,还可以读取slice和map的数据:

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

    for配合range可以用于读取slicemap的数据:

    fruit := map[string]int{"apple":5,"orange":7,"pineapple":3}
    for k,v := range fruit{
        println(k,v)
    

    "_"的使用

    由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用_来丢弃不需要的返回值 例如

    fruit := map[string]int{"apple":5,"orange":7,"pineapple":3}
    for _,v := range fruit{
        println(v)
    

    "_"相当于占位符的作用,这个位置必须要有一个值来接收,但是这个值又没有用,可以用 “ _”来占着位置。

    switch

    跟别的语言中的switch别无他致:

    var sex byte
    switch sex {
    case 0:
    	println("男生")
    case 1:
    	println("女生")
    default:
    	println("纳尼。。。。。")
    

    2.2 函数

    函数是Go里面的核心设计,它通过关键字func来声明,它的格式如下:

    func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
        //这里是处理逻辑代码
        //返回多个值
        return value1, value2
    

    上面的代码我们看出:

  • 关键字func用来声明一个函数funcName
  • 函数可以有一个或者多个参数,每个参数后面带有类型,通过,分隔
  • 函数可以返回多个值
  • 上面返回值声明了两个变量output1output2,如果你不想声明也可以,直接就两个类型
  • 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号
  • 如果没有返回值,那么就直接省略最后的返回信息
  • 如果有返回值, 那么必须在函数的外层添加return语句
  • 来看一个最简单的函数:

    package main
    func add(a,b int) map[] {
    	var resultMap = map[string]int{}
    	resultMap["add"] = a + b
    	resultMap["multi"] = a * b
    	resultMap["subtract"] = a - b
    	resultMap["division"] = a / b
    	return resultMap
    func main()  {
    	a := 3
    	b := 4
    	count := add(a, b)
    	println(count)
    

    多个返回值:

    Go中函数可以有多个返回值,这个比java强悍100倍:

    package main
    func add(a,b int) (int,int) {
    	return a+b,a-b
    func main()  {
    	a := 3
    	b := 4
    	add,sub := add(a, b)
    	println(add,sub)
    

    可变参数:

    跟Java中差不多吧:

    func myFunc(arg ...int) {}
    

    defer

    defer是golang的一个特色功能,被称为“延迟调用函数”。当外部函数返回后执行defer。类似于其他语言的 try… catch … finally… 中的finally,当然差别还是明显的。

    释放占用资源:

    func test() error {
    	file, err := os.Open("path")
    	if err != nil {
    		return err
    	//放在判断err状态之后
    	defer file.Close()
    	//todo
    	//...
    	return nil
    	//defer执行时机
    

    异常处理:

    func test2() {
    	defer func() {
    		if err := recover(); err != nil {
    			println(err)
    	}()
    	file, err := os.Open("path")
    	if err != nil {
    		panic(err)
    	defer file.Close()
    	//todo
    	//...
    	return
    	//defer执行时机
    

    日志输出:

    func test3() {
    	t1 := time.Now()
    	defer func() {
    		println("耗时: %f s", time.Now().Sub(t1).Seconds())
    	}()
    	//todo
    	//...
    	return
    	//defer执行时机
    

    3. struct类型

    Java中我们会去声明一些bean对象,里面包含字段和属性,在Go中可以声明一个struct类型的实体,在这个实体中声明一些属性,但是不可以在里面定义func。

    type person struct {
        name string
        age int
    

    使用方式:

    package main
    type person struct {
    	name string
    	age int
    func main()  {
    	var p person
    	p.age = 13
    	p.name = "xiaoming"
    	println(p.name,p.age)
    	p1 := person{"xiaoming",13}
    	println(p1.name,p1.age)
    	p2 := person{age:13,name:"xiaoming"}
    	println(p2.name,p2.age)
    

    Go中的继承—匿名字段

    上面我们在定义struct的时候,里面的属性都是字段名和类型一一对应的。实际上Go也支持只提供类型而不写字段名的方式,也就是匿名字段。

    当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。

    package main
    type PersonOther struct {
    	phone string
    	address string
    type Person struct {
    	PersonOther
    	name string
    	age int
    func main()  {
    	p1 := Person{PersonOther{"13242342123","xxxxxxx"},"xiaoming",13}
    	println(p1.address,p1.phone,p1.name,p1.age)
    

    可以看到在p1中是可以看到address和phone属性的,这就跟Java中的继承一样。

    既然说到继承,那么肯定会有这样的一种情况,就是在Person和PersonOther中都定义过了phone属性,当我们用p1去获取的时候到底获取的是哪个对象的phone属性呢?

    Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过Person.phone访问的时候,是访问student里面的字段,而不是PersonOther里面的字段。

    当前如果你想访问父类中的phone也不是不可以,Go还保留着父类中的对象呢,你可以这样取出来:

    parentPhone := p1.PersonOther.phone
    

    在Java中如果是这样的话,父类的同名字段就被子类覆写了,取不出来。

    4. 也谈谈面向对象编程----多态

    在Java中我们经常这样做:

    定义一个关于计算面积的接口;

    定义一个计算圆面积的类实现接口;

    定义一个正方形计算面积的类实现接口;

    定义一个计算长方形面积的类实现接口;

    这样我们就抽象出来一套统一的计算面积的方案由一个接口把持,需要哪个面积计算就调用相关的实现。

    在Go中同样也有这样的概念,但是实现方式确是不同。基于上面的原因所以就有了method的概念,method是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在func后面增加了一个receiver(也就是method所依从的主体)。

    method的语法如下:

    func (r ReceiverType) funcName(parameters) (results)
    

    看一个具体的示例:

    package main
    type Circle struct {
    	redius float64
    type Square struct {
    	width,height float64
    func (c Circle) area() float64  {
    	return c.redius * c.redius * 3.14
    func (s Square) area() float64  {
    	return s.height * s.width
    func main()  {
    	c := Circle{3.55}
    	s := Square{3,5}
    	println(c.area())
    	println(s.area())
    

    可以看到调用method通过Circle示例访问就像访问struct里面的字段一样。