go语言学习笔记

Posted 坠落鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了go语言学习笔记相关的知识,希望对你有一定的参考价值。

go语言学习笔记

go语言学习笔记(初级)

最近一直在学习go语言,因此打算学习的时候能够记录
一下笔记。我这个人之前是从来没有记录笔记的习惯,
一直以来都是靠强大的记忆力去把一些要点记住。
读书的时候因为一直都是有一个很安静和很专心的环境,
因此很多事情都能记得很清楚,思考的很透彻。但是随着
年纪不断增加,也算是经历了很多的事情,加上工作有时会让人
特别烦闷,很难把心好好静下来去学习,去思考大自然的终极
奥秘,因此需要记录一些东西,这些东西一方面可以作为一种自我激励
的机制,另一方面,也算是自己成长的轨迹吧。

一. 顺序编程

1. 变量

go语言变量定义的关键字是var。类型放在变量名后:

   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  //map
   var v8 func(a int) int  //函数

每一行不需要以分号作为结尾。 var
关键字还有一种是把多个变量的申明放在一起,

  var(
      v1 string
      v2 int
    )

2. 变量初始化

有人说,变量初始化有什么好提的,那么简单。是的,但是这里面确实还是有一些值得注意的点。

var a int = 10  //完整定义
var a = 10  //自动推断是int型
a := 10    //自动推断是int型,申明并未该变量赋值

第三种初始化方式无疑是最简单的。
但是要注意的是,这里面第三种方式是和特别的,比如

var a int
a := 10

等价于

var a int
var a int
a = 10

这时候就会报一个重复定义的错误。

3. 变量赋值

变量赋值和我们通常的语言基本是一致的,但是多了多重赋值功能。

i,j=j,i

这就直接实现了两个变量的交换。

4. 匿名变量

go语言的函数是多返回值的,因此可能有些值并没有被用到,这时我们就需要一个占位符去忽略这些返回值。

func GetName() (firstName, lastName, nickName string) {
 return "May", "Chan", "Chibi Maruko"
}
_, _, nickName := GetName()

5. 定义常量

通过const关键字,可以用来定义常量。

const Pi float64 = 3.1415926
const zero = 0.0 //自动推断类型
const (             //多定义
  size int64 = 10
  hello = -1
)
const u , v float32 = 0.0 , 3.0 //多重赋值
const a , b , c = 1 , 2 , “hello” //自动推断类型

常量的定义也可以跟一个表达式, 但是这个表达式应该是编译的时候就可以求值的.

const mask = 1 << 3 //正常
const Home = os.GetEnv("HOME") //错误,运行时才能确定

6. 预定义常量

这里要讲一个很有意思的东西, 叫做iota.

这个东西每一个const出现的位置被置为0,没出现一个iota出现,都自增1,到写一个const出现的时候,又置为0.

const (
  c1 = iota //0
  c2 = iota //1
  c3 = iota //2
)
const x = iota // x == 0 (因为iota又被重设为0了)
const y = iota // y == 0 (同上)

如果两个const赋值表达式是一样的,可以省略后面的赋值表达式.

const (
  c1 = iota //0
  c2 //1
  c3 //3
)
const (
  a = 1 <<iota // a == 1 (iota在每个const开头被重设为0)
  b // b == 2
  c // c == 4
)

6. 枚举

const (
  Sunday = iota
  Monday
  Tuesday
  Wednesday
  Thursday
  Friday
  Saturday
  numberOfDays // 这个常量没有导出
)

大写字母开头的包外可见, 小写字母开头的包外不可见.

7. 类型

  • 整型

    int8, uint8, int16, uint16,int32, uint32, int64, uint64, int, uint, uintptr

    不同类型不能相互比较.

  • 浮点类型

    float32, float64

    涉及的一些运算比较简单, 我们不做细讲.

  • 字符串类型

    下面我们展示一个完整的go语言程序, 也是以hello
    world为主题, 毕竟hello world是一个万斤油的主题.

    package main
    
    import "fmt" //引入依赖包
    
    func main() {
      fmt.Println("hello,world!")
    }

    这基本上是一个最简单的程序了,但是对于我们的学习非常有用,用这个模板可以写出非常好的东西出来.

    字符串串本身非常简单,主要就是一些字符串操作, 比如取特定位置的字符等.

    package main
    
    import "fmt" //引入依赖包
    
    func main() {
      var str string = "hello,world!"
      fmt.Println(str)
      ch := str[0]  //取某个特定位置的字符
      fmt.Printf("%c\\n",ch)
      length := len(str)
      fmt.Println(length) //len用来获取长度
      str = "你好,世界"
      ch = str[0]
      fmt.Printf("%c\\n",ch)
      length = len(str)
      fmt.Println(length)
    }

    输出结果为:

    hello,world!
    h
    12
    ä
    13

    这正好说明[]和len都不能处理中文.

    字符串连接也是用+.

    字符串的遍历:

    package main
    
    import "fmt" //引入依赖包
    
    func main() {
      var str string = "hello,world!"
      n := len(str)
      for i := 0; i < n; i++ {
        ch := str[i]
        fmt.Printf("%c\\n",ch)
      }
    }

    输出结果:

    h
    e
    l
    l
    o
    ,
    w
    o
    r
    l
    d
    !

    对于中文, 结果是乱码, 因为是一个字节一个字节输出的, 但是默认是UTF8编码, 一个中文对应3个字节.
    这里我们要提到一个range的关键字, 它可以把字符串按键值对的方式返回.

    package main
    
    import "fmt" //引入依赖包
    
    func main() {
      var str string = "hello,world! 你好,世界!"
      for _, ch := range str {
        fmt.Printf("%c\\n",ch)
      }
    }

    输出结果为:

    h
    e
    l
    l
    o
    ,
    w
    o
    r
    l
    d
    !
    
    你
    好
    ,
    世
    界
    !

    事实上, 字符类型有两种, 一种就是byte(uint8), 另一种是rune. 第一种遍历字符串ch是byte, 而第二种是rune.

  • 数组

    数组这种类型是非常常见的

    [32]byte
    [2*N] struct { x, y int32 }
    [1000]*float64
    [3][5]int
    [2][2][2]float64

    数组的遍历和字符串一样,这里不再重复.

    数组是值类型,在赋值时会拷贝一份.

  • 数组切片

    数组切片的概念比较复杂, 它有点类似于c++中vector的概念, 但又不完全一样.

    我们这里详细提几点.

    1. 切片的创建

      切片有两种创建方式, 一种是基于数组创建, 另一种是用make创建.

      package main
      
      import "fmt" //引入依赖包
      
      func main() {
        //从数组创建
        var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10}
        var sa []int = myArray[5:]
        for _, e := range sa {
            fmt.Println(e)
        }
        fmt.Println(len(sa))
        fmt.Println(cap(sa))
        //从make创建
        var mySlice2 []int = make([]int, 5, 10)
        for _, e := range mySlice2 {
            fmt.Println(e)
        }
        fmt.Println(len(mySlice2))
        fmt.Println(cap(mySlice2))
        //赋值
        var mySlice3 []int = []int{1,2,3}
        for _, e := range mySlice2 {
            fmt.Println(e)
        }
      }

      slice是引用类型.

      package main
      
      import "fmt" //引入依赖包
      
      func test(a [10]int)  {
        a[0] = 10
      }
      
      func printArray(a [10]int){
        for _, e := range a {
            fmt.Println(e)
        }
      }
      
      func main() {
        var myArray [10]int = [10]int{1,2,3,4,5,6,7,8,9,10}
        printArray(myArray)
        test(myArray)
        printArray(myArray)
      }

      输出结果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10

      我们发现数组确实是按照值来传递. 那么如果是slice呢, 会发生什么?

      package main
      
      import "fmt" //引入依赖包
      
      func test(a []int)  {
        a[0] = 10
      }
      
      func printArray(a []int){
        for _, e := range a {
            fmt.Println(e)
        }
      }
      
      func main() {
        var myArray []int = []int{1,2,3,4,5,6,7,8,9,10}
        printArray(myArray)
        test(myArray)
        printArray(myArray)
      }

      输出结果:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      10
      2
      3
      4
      5
      6
      7
      8
      9
      10

      确实是按照引用来传递的.

      append函数可以往切片尾部增加元素.

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

      mySlice = append(mySlice, mySlice2...)

      ...表示把一个slice拆成元素来处理.

      package main
      
      import "fmt" //引入依赖包
      
      func main() {
        var slice1 []int = make([]int,5,10)
        var slice2 []int = []int{1,2,3}
      
        fmt.Println(slice1)
        fmt.Printf("%p\\n",slice1)
        slice1 = append(slice1,slice2...)
        fmt.Println(slice1)
        fmt.Printf("%p\\n",slice1)
        slice1 = append(slice1,slice2...)
        fmt.Println(slice1)
        fmt.Printf("%p\\n",slice1)
      }

      输出结果:

      [0 0 0 0 0]
      0xc820012190
      [0 0 0 0 0 1 2 3]
      0xc820012190
      [0 0 0 0 0 1 2 3 1 2 3]
      0xc82005e000

      在这里我们看到,slice的地址是所随着内内存的改变而变化的,因此是需要仔细思考的.我个人不觉得
      go语言这种特性有什么好的,反正也是奇葩极了. 不过slice还提供copy, 也算是一些弥补吧.

  • map

    go语言中,map使用非常简单.基本上看代码就会了.

    package main
    
    import "fmt" //引入依赖包
    
    //定义一个Person的结构体
    type Person struct{
      name string
      age int
    }
    
    func main() {
      var dic map[string]Person = make(map[string]Person , 100) //初始化map
      dic["1234"] = Person{name:"lilei",age:100}
      dic["12345"] = Person{name:"hanmeimei",age:20}
      dic["123456"] = Person{name:"dagong",age:30}
      fmt.Println(dic)
      //删除dagong
      delete(dic,"123456")
      fmt.Println(dic)
      //查找某个key
      value,ok := dic["123456"]
      if ok {
          fmt.Println(value)
      }
      value,ok = dic["1234"]
      if ok {
          fmt.Println(value)
      }
    
      for k,v := range dic {
        fmt.Println(k + ":" + v.name)
      }
    }

    输出结果为:

    map[12345:{hanmeimei 20} 123456:{dagong 30} 1234:{lilei 100}]
    map[1234:{lilei 100} 12345:{hanmeimei 20}]
    {lilei 100}
    12345:hanmeimei
    1234:lilei

    map很简单吧. 数据结构我们讲完了, 接下来可以看看代码的程序控制了.

8. 程序控制

程序控制本人只提一些关键性的东西,不会啰嗦太多.

  • switch语句

    switch语句不需要在每个case地下写break,默认就是执行break.如果要执行多个case, 在case最后加入fallthrough.
    条件表达式不限制为常量或者整数.单个case自然可以有多个结果可以选.

    package main
    
    import "fmt" //引入依赖包
    
    func test(a int)  {
        switch {
          case a < 0:
              fmt.Println("hello")
          case a == 10:
              fallthrough
          case a > 10 && a < 100:
              fmt.Println("world")
          default:
              fmt.Println("nima")
        }
    }
    
    func main() {
        test(-1)
        test(10)
        test(100)
    }
  • 循环

    go语言的循环比较特别, 它用一个for就把for和while的活都干了.

    package main
    
    import "fmt" //引入依赖包
    
    func main() {
        sum := 0
        for i := 0; i <= 100; i++ {
            sum += i
        }
        fmt.Println(sum)
    
        sum = 0
        i := 0
        for(i <= 100){
          sum += i
          i++
        }
        fmt.Println(sum)
    }

    break还支持break到指定的label处.

    for j := 0; j < 5; j++ {
     for i := 0; i < 10; i++ {
     if i > 5 {
     break JLoop
     }
     fmt.Println(i)
     }
    }
    JLoop:
    // ...
  • 函数

    函数是一个非常重要的概念, 也很简单. go的函数以func关键字定义, 支持不定参数和多返回值. 函数名首字符的大小写是很有讲究的:

    小写字母开头的函数只在本包内可见,大写字母开头的函数才能被其他包使用。这个规则也适用于类型和变量的可见性。

    package main
    
    import "fmt" //引入依赖包
    
    //...int是不定参数,实际上就是一个slice, a,b是多返回值
    func SumAndAverage(sample ...int) (a , b float64)  {
      a , b = 0 , 0
      for _, d := range sample {
        a += float64(d)
      }
      if len(sample) == 0 {
        b = 0
      }else{
          b = a / float64(len(sample))
      }
      return a , b
    }
    
    func main() {
      a , b := SumAndAverage(1, 2 , 3)
      fmt.Println(a , b)
    }

    很简单吧. 注意, 如果是函数里面调了其他函数, 那么这个sample怎么传给其他喊函数呢?

    sample... //...表示是拆成一个个元素传递

    匿名函数的概念也很简单, 只要看代码就会明白.

    package main
    
    import "fmt" //引入依赖包
    
    func main() {
      var myFunc func(...int)(float64, float64)= func(sample ...int) (a , b float64)  {
        a , b = 0 , 0
        for _, d := range sample {
          a += float64(d)
        }
        if len(sample) == 0 {
          b = 0
        }else{
            b = a / float64(len(sample))
        }
        return a , b
      }
    
      a , b := myFunc(1, 2 , 3)
      fmt.Println(a , b)
    }

    下面是关于闭包的概念. 这个概念在许式伟的书中被诠释的非常好:

    闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,
    而是在定义代码块的环境中定义。
    要执行的代码块(由于自由变量包含在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定定的计算环境(作用域)。
    闭包的实现确保只要闭包还被使用,那么被闭包引用的变量会一直存在.
    *

    我们来看来两个闭包的例子.

    package main
    
    import "fmt" //引入依赖包
    
    func test(i int) func()  {
      return func(){
        fmt.Println(10+i)
        fmt.Printf("%p\\n",&i)
      }
    }
    
    func main() {
      a := test(1);
      b := test(2)
      a()
      b()
    }

    输出结果:

    11
    0xc82000a288
    12
    0xc82000a2c0

    我们从这个结果中发现, i的地址是会变的, 因为是作为一个局部变量传进去的.

    package main
    
    import "fmt" //引入依赖包
    
    func test(x int) func(int) int {
      return func(y int) int {
        fmt.Printf("%p\\n",&x)
        return x + y
      }
    }
    
    func main() {
      a := test(1);
      fmt.Println(a(10))
      fmt.Println(a(20))
    }

    输出结果:

    0xc82000a288
    11
    0xc82000a288
    21

    因为x只传入了一次, 因此没有改变.

    package main
    
    import (
     "fmt"
    )
    
    func main() {
       var j int = 5
       a := func() (func()) {
            var i int = 10
            return func() {
              fmt.Printf("i, j: %d, %d\\n", i, j)
            }
       }()
       a()
       j *= 2
       a()
     }

    此时输出:

    i, j: 10, 5
    i, j: 10, 10

二. 面向对象编程

这里我们先提值语义引用语义的概念.

b = a
b.Mofify()

如果b改变, a不发生改变, 就是值语义. 如果b改变, a也发生改变, 就是引用语义.

go语言大多数类型都是值语义, 比如:

基本类型: byte, int, float32, float64, string
复合类型: struct, array, pointer

也有引用语义的, 比如:
slice, map, channel, interface.

这是我们要牢记的.

我们的笔记整体式按照许式伟的书来安排, 但是由于许的书提纲性很强, 内容上不是很详细, 基本上会重新整理补充一些东西进去.

  • 结构体

结构体是用struct来申明的, 不多废话, 直接上代码.

package main

import (
 "fmt"
)

//申明一个结构体
type Person struct{
  Name string
  Age int
}

func main() {

  //结构体的初始化方式
  //1. 直接赋值
  var p Person
  p.Name = "dingding"
  p.Age = 10
  fmt.Println(p)

  //2.顺序赋值
  p1 := Person{"dingding",10}
  fmt.Println(p1)

  //3. key value 赋值
  p2 := Person{Name:"dingding",Age:10}
  fmt.Println(p2)

  //4.指针赋值
  p3 := &Person{Name:"dingding",Age:10}
  fmt.Println(p3)
  p4 := new(Person)
  fmt.Println(p4)

  fmt.Println("---------------------------")

  a := p
  a.Name = "dongdong"
  b := p3
  b.Name = "dongdong"
  fmt.Println(p)
  fmt.Println(p3)

}

输出结果:

{dingding 10}
{dingding 10}
{dingding 10}
&{dingding 10}
&{ 0}
---------------------------
{dingding 10}
&{dongdong 10}

这说明,struct确实是值语义.

下面讨论一下结构体的组合问题. 这点许的书中并没有过多涉及, 但是还是很有必要的, 因为在实际场合中用的会很多.

package main

import (
 "fmt"
)

//申明一个结构体
type Human struct{
  Name string
  Age int
  Phone string
}

//再申明一个结构体
type Employee struct {
    Person Human  // 匿名字段Human
    Speciality string
    Phone string  // 雇员的phone字段
}

func main() {
  e := Employee{
    Person:Human{
      Name:"dingding",
      Age:11,
      Phone:"6666666",
    },
    Speciality:"aaa",
    Phone:"77777777",
  }
  fmt.Println(e.Phone)

}

这段代码看上去非常ok, 但是如果我们稍微改一下代码呢? 比如把
Person改成匿名结构, 会有些好玩的现象.

package main

import (
 "fmt"
)

//申明一个结构体
type Human struct{
  Name string
  Age int
  Phone string
}

//再申明一个结构体
type Employee struct {
    Human  // 匿名字段Human
    Speciality string
    //Phone string  // 雇员的phone字段
}

func main() {
  e := Employee{
    Human{"dingding",11,"6666666"},
    "aaa",
    //Phone:"77777777",
  }
  fmt.Println(e.Phone)

}

此时输出的是6666666, 因为相当于它把Human的字段Phone继承下来了.
如果Employee里也定义Phone字段呢?

package main

import (
 "fmt"
)

//申明一个结构体
type Human struct{
  Name string
  Age int
  Phone string
}

//再申明一个结构体
type Employee struct {
    Human  // 匿名字段Human
    Speciality string
    Phone string  // 雇员的phone字段
}

func main() {
  e := Employee{
    Human{"dingding",11,"6666666"},
    "aaa",
    "77777777",
  }
  fmt.Println(e.Phone)

}

此时输出的时77777777, 因为相当于是覆盖. 那么怎么输出6666666呢?

package main

import (
 "fmt"
)

//申明一个结构体
type Human struct{
  Name string
  Age int
  Phone string
}

//再申明一个结构体
type Employee struct {
    Human  // 匿名字段Human
    Speciality string
    Phone string  // 雇员的phone字段
}

func main() {
  e := Employee{
    Human{"dingding",11,"6666666"},
    "aaa",
    "77777777",
  }
  fmt.Println(e.Phone)
  fmt.Println(e.Human.Phone)
}

输出结果:

77777777
6666666

所以, 匿名结构体的组合相当于有继承的功能.

  • 为类型添加方法

这个概念和java或者是C++非常不一样, 它的理念是把似乎是把方法绑定到特定类型上去.
这个概念已经不仅仅是对象的概念了, 事实上,
我也不知道google这帮人脑子是怎么想的, 这种搓劣的复古风格,
也是让我打开眼界, 我个人觉得, go虽然仗着google的名气, 似乎显得很厉害, 但是,
比起java和C++, 简直就是个玩具, 说的不好听一点,
比起scala这样的一出生就是个大胖子, go更像是个缺胳膊少腿的畸形儿.

好了, 不说了, 直接上代码.

package main

import (
 "fmt"
)

//go绑定方法必须是本包内的,int不是main包内定义的.
//因此需要type一下, Integer就是本包内定义的类型
type Integer int

//为int绑定一个Print方法
func (i Integer) Println() {
  fmt.Println(i)
}

func main() {
  var a Integer = 10
  a.Println()
}

结果输出10, 如果是如下呢?

package main

import (
 "fmt"
)

//go绑定方法必须是本包内的,int不是main包内定义的.
//因此需要type一下, Integer就是本包内定义的类型
type Integer int

//为int绑定一个Print方法
func (i Integer) Println() {
  fmt.Println(i)
}

func main() {
  a := 10
  a.Println()
}

输出结果:

# command-line-arguments
./main.go:18: a.Println undefined (type int has no field or method Println)

因为a := 10, go会把a推断为int, 但int并没绑上Println方法.

注意:

//为int绑定一个Print方法
func (i Integer) Println() {
  fmt.Println(i)
}

这里的i并不是引用类型,因此对i的修改不会反应到a上去.

package main

import (
 "fmt"
)

//go绑定方法必须是本包内的,int不是main包内定义的.
//因此需要type一下, Integer就是本包内定义的类型
type Integer int

//为int绑定一个Print方法
func (i Integer) Add() {
  i += 2
}

func main() {
  var a Integer = 10
  a.Add()
  fmt.Println(a)
}

以上是关于go语言学习笔记的主要内容,如果未能解决你的问题,请参考以下文章

Go语言技巧之正确高效使用slice(听课笔记总结--简单易懂)

学习笔记:python3,代码片段(2017)

《Go语言精进之路》读书笔记 | 使用Go语言原生编码思维来写Go代码

《Go语言精进之路》读书笔记 | 使用Go语言原生编码思维来写Go代码

Go学习笔记Go命令工具

Go基础--笔记