最新文章专题视频专题问答1问答10问答100问答1000问答2000关键字专题1关键字专题50关键字专题500关键字专题1500TAG最新视频文章推荐1 推荐3 推荐5 推荐7 推荐9 推荐11 推荐13 推荐15 推荐17 推荐19 推荐21 推荐23 推荐25 推荐27 推荐29 推荐31 推荐33 推荐35 推荐37视频文章20视频文章30视频文章40视频文章50视频文章60 视频文章70视频文章80视频文章90视频文章100视频文章120视频文章140 视频2关键字专题关键字专题tag2tag3文章专题文章专题2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章专题3
当前位置: 首页 - 正文

go学习笔记

来源:动视网 责编:小OO 时间:2025-10-02 00:09:50
文档

go学习笔记

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

const zero = 0.0 //自动推断类型

const (             //多定义

  size int = 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 <  b // b == 2

  c // c == 4

)

6. 枚举

const (

  Sunday = iota

  Monday

  Tuesday

  Wednesday

  Thursday

  Friday

  Saturday

  numberOfDays // 这个常量没有导出

)

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

7. 类型

∙整型

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

不同类型不能相互比较.

∙浮点类型

float32, float

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

∙字符串类型

下面我们展示一个完整的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]*float

[3][5]int

[2][2][2]float

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

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

∙数组切片

数组切片的概念比较复杂, 它有点类似于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 float)  {

  a , b = 0 , 0

  for _, d := range sample {

    a += float(d)

  }

  if len(sample) == 0 {

    b = 0

  }else{

      b = a / float(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)(float, float)= func(sample ...int) (a , b float)  {

    a , b = 0 , 0

    for _, d := range sample {

      a += float(d)

    }

    if len(sample) == 0 {

      b = 0

    }else{

        b = a / float(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, float, 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)

}

输出10.

如果我们用引用呢?

package main

import (

 "fmt"

)

//go绑定方法必须是本包内的,int不是main包内定义的.

//因此需要type一下, Integer就是本包内定义的类型

type Integer int

//为int绑定一个Print方法

func (this *Integer) Add() {

  *this += 2

}

func main() {

  var a Integer = 10

  a.Add()

  fmt.Println(a)

}

这时输出12. 我们发现, 这个this就像是我们C++里面的this指针一样.

不过也傻13复古的多.

我们在看一个例子,

package main

import (

 "fmt"

)

//go绑定方法必须是本包内的,int不是main包内定义的.

//因此需要type一下, Integer就是本包内定义的类型

type Integer int

//为int绑定一个Print方法

func (this *Integer) Add() {

  fmt.Println(this)

  *this += 2

}

func main() {

  var b Integer = 10

  var a *Integer = &b

  a.Add()

  fmt.Println(a)

}

输出结果:

0xc82000a288

0xc82000a288

我们发现,

//为int绑定一个Print方法

func (this *Integer) Add() {

  fmt.Println(this)

  *this += 2

}

该方法用指针调用和用值去调用, 效果是一样的. this都是指向原来对象的指针而已.

下面这个例子来自于许的书中,

package main

type Integer int

func (a Integer) Less(b Integer) bool {

return a < b

}

func (a *Integer) Add(b Integer) {

 *a += b

}

type LessAdder interface {

 Less(b Integer) bool

 Add(b Integer)

}

func main() {

  var a Integer  = 1

  var b LessAdder = a

}

输出:

# command-line-arguments

./main.go:20: cannot use a (type Integer) as type LessAdder in assignment:

Integer does not implement LessAdder (Add method has pointer receiver)

这个例子似乎有点奇怪, 为什么呢?

package main

type Integer int

func (a Integer) Less(b Integer) bool {

return a < b

}

func (a *Integer) Add(b Integer) {

 *a += b

}

type LessAdder interface {

 Less(b Integer) bool

 Add(b Integer)

}

type Less interface {

 Less(b Integer) bool

}

type Adder interface {

Add(b Integer)

}

func main() {

  var a Integer  = 1

  var b Adder = a

  b.Add(10)

}

我们可以看得更清楚:

./main.go:28: cannot use a (type Integer) as type Adder in assignment:

Integer does not implement Adder (Add method has pointer receiver)

但如果是:

package main

type Integer int

func (a Integer) Less(b Integer) bool {

return a < b

}

func (a *Integer) Add(b Integer) {

 *a += b

}

type LessAdder interface {

 Less(b Integer) bool

 Add(b Integer)

}

type Less interface {

 Less(b Integer) bool

}

type Adder interface {

Add(b Integer)

}

func main() {

  var a Integer  = 1

  var b Integer = a

  b.Add(10)

}

就没有任何问题. 对比起来, 就是这两行代码:

var b Integer = a

var b Adder = a

我们接下去会娓娓道来其中的奥妙.

package main

import(

  "fmt"

)

//定义对象People、Teacher和Student

type People struct {

  Name string

}

type Teacher struct{

  People

  Department string

}

type Student struct{

  People

  School string

}

//对象方法实现

func (p *People) SayHi() {

  fmt.Printf("Hi, I'm %s. Nice to meet you!\\n",p.Name)

}

func (t *Teacher) SayHi(){

  fmt.Printf("Hi, my name is %s. I'm working in %s .\\n", t.Name, t.Department)

}

func (s *Student) SayHi() {

  fmt.Printf("Hi, my name is %s. I'm studying in %s.\\n", s.Name, s.School)

}

func (s *Student) Study() {

  fmt.Printf("I'm learning Golang in %s.\\n", s.School)

}

//定义接口Speaker和Learner

type Speaker interface{

  SayHi()

}

type Learner interface{

  SayHi()

  Study()

}

func main() {

    //初始化对象

    people := People{"张三"}

  //  teacher := &Teacher{People{"郑智"}, "Computer Science"}

  //  student := &Student{People{"李明"}, "Yale University"}

    var speaker Speaker   //定义Speaker接口类型的变量

    speaker = people

    speaker.SayHi()

}

这时就会出现上面我们提到的错误. 尽管如果我们这么去调用:

package main

import(

  "fmt"

)

//定义对象People、Teacher和Student

type People struct {

  Name string

}

type Teacher struct{

  People

  Department string

}

type Student struct{

  People

  School string

}

//对象方法实现

func (p *People) SayHi() {

  fmt.Printf("Hi, I'm %s. Nice to meet you!\\n",p.Name)

}

func (t *Teacher) SayHi(){

  fmt.Printf("Hi, my name is %s. I'm working in %s .\\n", t.Name, t.Department)

}

func (s *Student) SayHi() {

  fmt.Printf("Hi, my name is %s. I'm studying in %s.\\n", s.Name, s.School)

}

func (s *Student) Study() {

  fmt.Printf("I'm learning Golang in %s.\\n", s.School)

}

//定义接口Speaker和Learner

type Speaker interface{

  SayHi()

}

type Learner interface{

  SayHi()

  Study()

}

func main() {

  //初始化对象

  people := People{"张三"}

//  teacher := &Teacher{People{"郑智"}, "Computer Science"}

//  student := &Student{People{"李明"}, "Yale University"}

  //var speacker Speaker   //定义Speaker接口类型的变量

  //speacker = people

  people.SayHi()

}

或者

package main

import(

  "fmt"

)

//定义对象People、Teacher和Student

type People struct {

  Name string

}

type Teacher struct{

  People

  Department string

}

type Student struct{

  People

  School string

}

//对象方法实现

func (p *People) SayHi() {

  fmt.Printf("Hi, I'm %s. Nice to meet you!\\n",p.Name)

}

func (t *Teacher) SayHi(){

  fmt.Printf("Hi, my name is %s. I'm working in %s .\\n", t.Name, t.Department)

}

func (s *Student) SayHi() {

  fmt.Printf("Hi, my name is %s. I'm studying in %s.\\n", s.Name, s.School)

}

func (s *Student) Study() {

  fmt.Printf("I'm learning Golang in %s.\\n", s.School)

}

//定义接口Speaker和Learner

type Speaker interface{

  SayHi()

}

type Learner interface{

  SayHi()

  Study()

}

func main() {

  //初始化对象

  people := People{"张三"}

//  teacher := &Teacher{People{"郑智"}, "Computer Science"}

//  student := &Student{People{"李明"}, "Yale University"}

  var speacker Speaker   //定义Speaker接口类型的变量

  speacker = &people

  speacker.SayHi()

}

这样就都没有任何问题, 这就是说什么呢? 这说明对于对象的方法, 无论接受者是对象还是对象指针, 都没

任何问题. 但是如果是借口,如果接口中存在某个方法,绑定的接收者是对象指针,那么这个接口

也只能被该对象指针赋值. 如此奇葩的设计, 我只能说, go的设计者真是个脑残.

∙继承

好了, 下面正式讲继承的语法, 话说那玩意儿的真的算不上继承, 比C++的继承真的时不知道low到哪里去了. 但是我也不知道为啥这是go爱好者们最爱标榜的东西,

有时我想想, 程序员也真是单纯的人, 一点点的蛊惑, 就会让他们激动不已,

感觉就要去参加了似的.

go的继承非常简陋, 就是一个匿名结构组合的问题. 不废话,直接上代码.

package main

import(

  "fmt"

)

//基类

type Base struct{

  Name string

}

//绑定Say方法

func (b *Base) Say()  {

  fmt.Println(b.Name)

}

//绑定ok方法

func (b *Base) Ok()  {

  fmt.Println("ok")

}

//Foo有个匿名结构Base

type Foo struct{

  Base

  Name string

}

//重写Say方法

func (f *Foo) Say()  {

  f.Base.Say()

  fmt.Println(f.Name)

}

func main() {

  var f *Foo = &Foo{Base{"father"},"sun"}

  f.Ok()

  f.Say()

}

输出结果:

ok

father

sun

ok,下面我们看看多继承二义性的问题.

package main

import(

  "fmt"

)

//father

type Father struct{

}

func (f *Father)Say()  {

  fmt.Println("father")

}

//mother

type Mother struct{

}

func (f *Mother)Say()  {

  fmt.Println("mother")

}

//sun

type Sun struct{

  Father

  Mother

}

func main() {

  var s *Sun = new(Sun)

  s.Say()

}

输出:

# command-line-arguments

./main.go:32: ambiguous selector s.Say

果然展现了二义性. 消歧义方式也是土的掉渣:

package main

import(

  "fmt"

)

//father

type Father struct{

}

func (f *Father)Say()  {

  fmt.Println("father")

}

//mother

type Mother struct{

}

func (f *Mother)Say()  {

  fmt.Println("mother")

}

//sun

type Sun struct{

  Father

  Mother

}

func main() {

  var s *Sun = new(Sun)

  s.Father.Say()

  s.Mother.Say()

}

我也是醉了.

此外, 我们也会看到还有一种用法,

package main

import(

  "fmt"

)

//基类

type Base struct{

  Name string

}

//绑定Say方法

func (b *Base) Say()  {

  fmt.Println(b.Name)

}

//绑定ok方法

func (b *Base) Ok()  {

  fmt.Println("ok")

}

//Foo有个匿名结构Base

type Foo struct{

  *Base  //base是个指针

  Name string

}

//重写Say方法

func (f *Foo) Say()  {

  f.Base.Say()

  fmt.Println(f.Name)

}

func main() {

  var f *Foo = &Foo{&Base{"father"},"sun"}

  f.Ok()

  f.Say()

}

这里Foo的Base是一个指针类型, 因此我们传入的也必须是个指针, 我们可以看到这种用法.

∙接口

这个我不想吐槽什么, 前面已经吐槽过了, 但是这种设计, 确实也是诡异之极.

go的借口是非侵入式的, 只要实现了接口定义的方法, 就等于实现了该接口. 换句话说, 接口的实现和定义式可以分离

不相关的.

接口的例子还是之前那个:

package main

import(

  "fmt"

)

type Integer int

func (a Integer) Less(b Integer) bool {

return a < b

}

func (a *Integer) Add(b Integer) {

  *a += b

}

//定义接口

type LessAdder interface {

  Less(b Integer) bool //函数声明

  Add(b Integer) //函数声明

}

func main() {

  var a Integer = 10

  var b LessAdder = &a //道理我们前面提到过了,Add接收者是个对象指针

  fmt.Println(b.Less(5))

  b.Add(20)

  fmt.Println(a)

}

输出:

false

30

只要两个接口拥

有相同的方法列表(次序不同不要紧),那么它们就是等同的,可以相互赋值。

接口赋值并不要求两个接口必须等价。如果接口A的方法列表是接口B的方法列表的子集,

那么接口B可以赋值给接口A。

几个接口也可以组合出一个接口.

type ReadWriter interface {

 Reader

 Writer

}

等价于:

type ReadWriter interface {

 Read(p []byte) (n int, err error)

 Write(p []byte) (n int, err error)

}

∙Any类型

由于Go语言中任何对象实例都满足接口interface{},所以interface{}看起来是任何对象的Any类型

var v1 interface{} = 1

var v2 interface{} = "abc"

var v3 interface{} = &v2

var v4 interface{} = struct{ X int }{1}

var v5 interface{} = &struct{ X int }{1}

func Printf(fmt string, args ...interface{})

func Println(args ...interface{})

∙接口转换和类型查询

接口转换

package main

import(

  "fmt"

)

type Integer int

func (a Integer) Less(b Integer) bool {

return a < b

}

func (a *Integer) Add(b Integer) {

  *a += b

}

//定义接口

type LessAdder interface {

  Less(b Integer) bool //函数声明

  Add(b Integer) //函数声明

}

//定义接口

type Adder interface {

  Add(b Integer) //函数声明

}

func main() {

  var a Integer = 10

  var b LessAdder = &a //道理我们前面提到过了,Add接收者是个对象指针

  if  c , ok = b.(Adder); ok{

    c.Add(10)

    fmt.Println(a)

    //fmt.Println(c.Less(100)) //报错,c.Less undefined (type Adder has no field or method Less)

  }

}

类型查询

package main

import(

  "fmt"

)

func main() {

  b := "a"

  var a interface{} = b

  switch a.(type) {

  case int:

    fmt.Println("int")

  case float32:

    fmt.Println("float32")

  case int32:

    fmt.Println("int32")

  case float:

    fmt.Println("float")

  case bool:

    fmt.Println("bool")

  case string:

    fmt.Println("string")

  default:

    fmt.Println("ok")

  }

}

∙补充

我们补充一些defer-panic-recover的案列.

package main

import (

  "fmt"

)

func f()  {

  fmt.Println("a")

}

func main() {

  defer func() {

    if err := recover(); err != nil{

      fmt.Println(err)

    }

  }()

  f()

  fmt.Println("b")

}

输出结果:

a

b

如果我们在f中panic呢? 这会发生什么呢?

package main

import (

  "fmt"

)

func f()  {

  fmt.Println("a")

  panic("error!")

}

func main() {

  defer func() {

    if err := recover(); err != nil{

      fmt.Println(err)

    }

  }()

  f()

  fmt.Println("b")

}

输出:

a

error!

我们发现, b没有输出. panic会抛出一个异常, 由recover去捕获.f抛出异常后, 事实上,

main剩下的部分都不会执行, 但是因为我们defer了,

defer是一定会执行的,因此我们在defer中捕获了panic抛出的

异常. 这就是为什么b没有输出. 似乎和try catch很像. 如果我们希望把b也输出, 但也能捕获异常呢?

package main

import (

  "fmt"

)

func f()  {

  fmt.Println("a")

  panic("error!")

}

func main() {

  func(){

    defer func() {

      if err := recover(); err != nil{

        fmt.Println(err)

      }

      }()

      f()

  }()

  fmt.Println("b")

}

输出结果:

a

error!

b

如果是这样呢?

package main

import (

  "fmt"

)

func f()  {

  fmt.Println("a")

  panic("error!")

}

func main() {

  func(){

      f()

      defer func() {

        if err := recover(); err != nil{

          fmt.Println(err)

        }

      }()

  }()

  fmt.Println("b")

}

此时, 在定义defer之前, f已经panic了, 没有recover去捕获, 这个panic会一直抛出.

直到被go虚拟机捕获.

输出:

a

panic: error!

goroutine 1 [running]:

main.f()

/Users/fengyan/code/go/test/main.go:9 +0x11e

main.main.func1()

/Users/fengyan/code/go/test/main.go:14 +0x18

main.main()

/Users/fe

go里面有个东西很好玩, nil类似于java的null, 那么java如果对null调用方法, 会直接抛出一个空指针异常.

那么go会怎么样呢?

package main

func main() {

  nil.Println("a")

}

输出结果:

# command-line-arguments

./main.go:4: use of untyped nil

看来还是不行的.

所以调用前我们还是要进行空的判断.

三. go并发编程

并发不是并行. 并发比并行更加优秀. 并发是时间片轮换, 并行是多核计算. 事实上, 并行可以由并发指定到多个cpu执行.

我们马上看一个具体的例子.

package main

import (

  "fmt"

)

func Add(x, y int)  {

  z := x + y

  fmt.Println(z)

}

func main() {

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

    go Add(i,i)

  }

}

结果我们发现什么都没有输出, 这是因为go是异步执行的, main不会等Add完成, 它会继续执行下去, 于是发生了main

函数先结束, 于是进程就结束了.

所以这里我们需要做同步, 所谓同步, 就是主线程去等待子线程完成(go里的线程其实是协程, 协程你可以认为是轻量级的线程).

线程间通信模型基本上常用的也就是两种,共享内存和消息队列.

后者目前看来比较流行, 比如akka(actor模型), zmq(消息队列模型)等 都是基于消息队列的并发模型.

go也是采用了消息队列的方式, 这里就是channel.

channel的申明形式:

var chanName chan ElementType

ElementType是该chan能够支持的数据类型.

chan的初始化有两种:

ch := make(chan int)

ch := make(chan int,capacity)

前者写入和读取是阻塞的, 后者自带了一个buffer,

如果没有达到capacity, 是非阻塞的, 达到capacity才会阻塞. 读取的话, 如果为buffer空,

会阻塞.

chan写入数据

ch <- value

chan读取数据

value : = <-ch

对于带buffer的chan也可以用for和range读取:

for i := range ch {

 fmt.Println("Received:", i)

}

下面我们把同步后的代码贴上:

package main

import (

  "fmt"

)

func Add(x, y int, ch chan int)  {

  z := x + y

  fmt.Println(z)

  //写入后就阻塞

ch <- 1

}

func main() {

  //chan int slice, make([]chan int, 10)是创建slice的方法

  //容量是10,超过这个容量,会发生内存拷贝

  //var chs []chan int = make([]chan int, 10)

  //其实这里用数组更好

  var chs [10]chan int

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

    chs[i] = make(chan int)

    go Add(i,i,chs[i])

  }

  //同步

  for _, ch := range chs{

    //如果线程没有写入数据, 会阻塞

<- ch

  }

}

输出结果:

18

0

2

4

12

14

16

10

8

6

下面我们介绍一下select语法,

select {

case <-chan1:

    //读取数据

case chan2 <- 1:

    //写入数据

  default:

    //默认操作

}

select会去注册事件, 比如是否可读,是否可写, 根据对应的事件去执行相应的代码.

package main

import (

  "fmt"

)

func RandomSignal(ch chan int)  {

  for{

    select{

      //写入1事件

case ch <- 1:

      //写入0事件

case ch <- 0:

    }

  }

}

func main() {

  //var ch chan int = make(chan int)效果一样

  //但是有些微妙的不同

  var ch chan int = make(chan int,1)

  go RandomSignal(ch)

  for value := range ch{

    fmt.Println(value)

  }

}

这是一个随机产生0或者1的信号发生器.

单向channel

顾名思义,单向channel只能用于发送或者接收数据。channel本身必然是同时支持读写的,

否则根本没法用。假如一个channel真的只能读,那么肯定只会是空的,因为你没机会往里面写数

据。同理,如果一个channel只允许写,即使写进去了,也没有意义,因为没有机会读取里面

的数据。所以的单向channel概念,其实只是对channel的一种使用。

var ch1 chan int // 双向

var ch2 chan<- float// 只写

var ch3 <-chan int // 只读

那么单向channel如何初始化呢?之前我们已经提到过,channel是一个原生类型,因此不仅

支持被传递,还支持类型转换。只有在有了单向channel的概念后,读者才会明白类型转换对于

channel的意义:就是在单向channel和?向channel之间进行转换。示例如下:

ch4 := make(chan int)

ch5 := <-chan int(ch4) // 只读

ch6 := chan<- int(ch4) //只写

关闭channel

close(ch)

如何判断一个channel是否已经被关

闭?我们可以在读取的时候使用多重返回值的方式:

x, ok := <-ch

package main

import (

  "fmt"

)

func RandomSignal(ch chan int)  {

for i:= 0; i <= 9; i++{

    select{

      //写入1事件

case ch <- 1:

      //写入0事件

case ch <- 0:

    }

  }

  close(ch)

}

func main() {

  //var ch chan int = make(chan int)效果一样

  //但是有些微妙的不同

  var ch chan int = make(chan int,1)

  go RandomSignal(ch)

  for value := range ch{

    fmt.Println(value)

  }

}

输出:

1

0

1

1

0

1

1

0

0

0

close(ch)之后, 对ch的for循环会终止.

并行计算:

package main

import (

  "runtime"

  "sync"

  "fmt"

)

type Vector []float

func min(a int, b int) int {

if a < b {

    return a

  }

  return b

}

func max(a int, b int) int {

if a < b {

    return b

  }

  return a

}

func mul(u, v Vector, k int) (res float) {

    n := min(k+1, len(u))

    j := min(k, len(v)-1)

for i := k - j; i < n; i, j = i+1, j-1 {

        res += u[i] * v[j]

    }

    return

}

func Convolve(u, v Vector) (w Vector) {

    n := len(u) + len(v) - 1

    w = make(Vector, n)

size := max(1, 1<<20/n)

    wg := new(sync.WaitGroup)

    wg.Add(1 + (n-1)/size)

for i := 0; i < n && i >= 0; i += size {

        j := i + size

if j > n || j < 0 {

            j = n

        }

        go func(i, j int) {

for k := i; k < j; k++ {

                w[k] = mul(u, v, k)

            }

            wg.Done()

        }(i, j)

    }

    wg.Wait()

    return

}

func main() {

  //numcpu := runtime.NumCPU()

 numcpu := 2

 runtime.GOMAXPROCS(numcpu) //设置使用多少个cpu核心

  var a Vector = make([]float,1000000)

  var b Vector = make([]float,1000000)

for i := 0; i < 1000000 - 1; i++ {

    a[i] = 1

    b[i] = 1

  }

  w := Convolve(a,b)

  fmt.Println(w)

}

runtime.GOMAXPROCS(numcpu)

该函数是用来设置cpu使用的核心个数, 在许的书中, 如果不设置这个环境变量, 默认是1个核心, 但是事实上,

在我这个版本的go语言中, 已经支持默认多核并发啦.

下面我们来看看同步的概念.

Go语言包中的sync包提供了两种?类型:

sync.Mutex和sync.RWMutex。sync.Mutex

是读写都锁, sync.RWMutex是写锁读不锁.

用法:

var l sync.Mutex

func foo() {

 l.Lock()

 defer l.Unlock()

 //...

}

对于从全局的角度只需要运行一次的代码,比如全局初始化操作,Go语言提供了一个Once

类型来保证全局的唯一性操作,具体代码如下:

var a string

var once sync.Once

func setup() {

 a = "hello, world"

}

func doprint() {

 once.Do(setup)

 print(a)

}

func twoprint() {

 go doprint()

 go doprint()

}

如果不考虑线程安全, 等价于:

var done bool = false //全局变量

func setup() {

 a = "hello, world"

 done = true

}

func doprint() {

 if !done {

 setup()

 }

print(a)

}

WaitGroup

var wg sync.WaitGroup

该类型有三个指针方法,即Add、Done和Wait。

类型sync.WaitGroup是一个结构体类型。在它之中有一个代表计数的字段。

当一个sync.WaitGroup类型的变量被声明之后,其值中的那个计数值将会是0。

我们可以通过该值的Add方法增大或减少其中的计数值。

虽然Add方法接受一个int类型的值,并且我们也可以通过该方法减少计数值,但是我们一定不要让计数值变为负数。因为这样会立即引发一个运行恐慌。

这也代表着我们对sync.WaitGroup类型值的错误使用。

除了调用sync.WaitGroup类型值的Add方法并传入一个负数之外,我们还可以通过调用该值的Done来使其中的计数值减一。

当我们调用sync.WaitGroup类型值的Wait方法的时候,它会去检查该值中的计数值。

如果这个计数值为0,那么该方法会立即返回,且不会对程序的运行产生任何影响。 但是,如果这个计数值大于0,

那么该方法的调用方所属的那个Goroutine就会被阻塞。

直到该计数值重新变为0之时,为此而被阻塞的所有Goroutine才会被唤醒。

我们来看第一个例子:

package main

import (

  "fmt"

  "sync"

)

func Add(x, y int, wg *sync.WaitGroup) {

  z := x + y

  fmt.Println(z)

  wg.Done()

}

func main() {

  var wg *sync.WaitGroup = &sync.WaitGroup{}

  wg.Add(10)

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

    go Add(i,i,wg)

  }

  wg.Wait()

}

初级教程我们就这样快速结束了, 咱们高级见. 嗯, 233333333.

文档

go学习笔记

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

最新推荐

猜你喜欢

热门推荐

专题
Top