Go语言基础2

函数

定义:

func-define-syntax

作用域

在Go中,定义在函数外的变量是全局的,定义在函数内部是局部的。如果全局与局部变量同名,局部变量将覆盖全局变量。

传值与传指针

Go中参数传递是值传递,当传一个参数值到被调用函数里的时候,实际上是拷贝了这个参数值。

变参

func myfunc(arg ...int) {}:变参的类型都相同,在这个例如的函数体中,变量arg是一个int类型的slice。

如果不指定变参的类型,默认是空的接口interface{}

传递变参:


func myfunc(arg ...int) {
    myfunc2(arg...)                // 按原样传递
    myfunc2(arg[:2]...)            // 传递片段
}

多个返回值

Go函数的返回值或者结果参数可以指定一个名字,并且像原始的变量那样使用,就像输入参数那样。

官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。

如果对其命名,在函数开始时,它们会用其类型的零值初始化;如果函数在不加入参数的情况下执行了return语句,结果参数的当前值会作为返回值返回。

函数作为值

在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型:
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])


package main
import "fmt"

type testInt func(int) bool      // 声明了一个函数类型

func isOdd(integer int) bool {
    if integer%2 == 0 {
        return false
    }
    return true
}

func isEven(integer int) bool {
    if integer%2 == 0 {
        return true
    }
    return false
}

// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
    var result []int
    for _, value := range slice {
        if f(value) {
            result = append(result, value)
        }
    }
    return result
}

func main(){
    slice := []int {1, 2, 3, 4, 5, 7}
    fmt.Println("slice = ", slice)
    odd := filter(slice, isOdd)              // 函数当做值来传递了
    fmt.Println("Odd elements of slice are: ", odd)
    even := filter(slice, isEven)            // 函数当做值来传递了
    fmt.Println("Even elements of slice are: ", even)
}

延迟执行

defer关键字后面指定的函数在函数退出前调用。延迟的函数是按照后进先出(LIFO)的顺序执行。

利用defer甚至可以修改返回值。


defer func(x int) {
        // 在这里匿名函数内部,可以访问外围函数的返回值。
} (5)             // () 在这里是必须的。

自定义类型 struct

在Go中也可以声明新的类型,作为其他类型的属性或字段的容器。

定义


type Student struct {
   name string
   age int
}

// 给自定义类型添加方法
func (s *Student)show() {
    fmt.Println("i'am " + s.name)
}

注意:
struct 不允许递归定义或定义里包含循环。下面两种情形都是编译不通过的。


//  形式一
type  Node  struct {
     parent,  lChild,  rChild  Node
     key  int
}


//  形式二
type  Node  struct {
     parent,  lChild,  rChild  BinaryTree
     key  int
}

type  BinaryTreestruct {
     node  Node
}

实例化

类型的初始化使用方式可以有三种:

  • var s Student:先声明变量,变量的属性值为默认值,然后再对属性赋值。
  • s := Student {"coderbee", 28}:按照声明的顺序提供属性的初始值。
  • s := Student {name:"coderbee", age:28}:按照field:value的形式初始化属性值,任意顺序。

匿名字段

如果省略字段的名字,可以创建匿名字段(默认字段名是类型的名字,所以一个struct里面不能定义两个相同类型的匿名字段)。

当匿名字段是一个struct的时候,这个struct所拥有的全部字段都被隐式地引入当前定义的这个struct。外围的struct可以像访问自己的属性那样访问匿名struct的属性。

如果内层struct与外层struct具有同名的字段,那么最外层的优先访问,对于内层字段的访问可以通过内层struct的字段名来访问。


package main

import "fmt"

type Human struct {
    name   string
    age    int
    weight int
}

type Student struct {
    Human
    spciality string
    weight int
}

func main() {
    mark := Student{Human{"mark", 25, 120}, "computer science", 125}
    fmt.Printf("mark: %v\n", mark)
    fmt.Printf("mark's name: %v\n", mark.name)
    mark.weight = 130
    fmt.Printf("out weight:%d, inner weight:%d, default weight:%d\n", mark.weight, mark.Human.weight, mark.weight)
}

方法

方法就是有接收者的函数,可以在任意类型上定义方法(除了非本地类型(定义在其他包的),包括内建类型)。

接口定义为一个方法的集合,方法包含实际代码。也就是说,一个接口是定义,而方法是实现。因此,接收者不能定义为接口类型,这样做会引起invalid receiver type的编译器错误。来自语言说明书:
接收者类型必须是T*T,这里的T是类型名。T叫做接收者基础类型或简称基础类型。基础类型一定不能是指针或接口类型,并且定义在与方法相同的包中。

给类型添加方法有几种途径:

  • 函数调用: func doSomething(s * Student, age int) { /* ... */ }
  • 方法调用,类型指针上添加: func (s * Student)doSomething(age int) { /* ... */ }
  • 方法调用,类型值上添加: func (s Student)doSomething(age int) { /* ... */ }

指针类型上添加和类型值上添加的区别在于: 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。

如果x可获取地址,并且&x的方法中包含了m,x.m()(&x).m()更短的写法。

如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method。
如果一个method的receiver是T,你可以在一个*T类型的变量P上面调用这个method,而不需要*P去调用这个method。

接口 interface

每个类型都有接口,意味着对那个类型定义了方法集合。interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。

定义


type I interface {
    Get() int
    Put(int)
}

type S struct { i int }
func (p *S) Get() int { return p.i }
func (p *S) Put(v int) { p.i = v }

对于接口I,S是合法的实现,因为它定义了I所需的两个方法。

每个类型都能匹配到空接口: interface{}。 空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。

无需明确一个类型是否实现了一个接口意味着Go实现了duck typing的模式。这不是纯粹的duck typing,因为如果可能的话Go编译器将对类型是否实现了接口进行静态检查。

类型判断

方法一:


func  getType(p  interface{}) string {
switch p.(type) {
    case int:  return "int"
    case string:  return "string"
    case Person:  return "Person"
    default:    return "unknow type"
}
}

switch之外使用(type)是非法的。

方法二:
另一种判断一个接口类型是否实现了某个特定接口的方法:


func runtimeType(any interface{}) {
   if _, ok := any.(I); ok {
       println("type is I .")
   } else if  _, ok := any.(int); ok {
       println("type is int .")
   }
}

.(I)是断言类型,用于转换any到I类型的接口。

继承

Go不支持继承,鼓励使用组合和委派。


type Engine interface {
  Start()
  Stop()
}

type Car struct {
  Engine
}

定义Car的时候,Engine是一个匿名成员,这是一个只能被其类型识别的成员。匿名成员和其他的成员一样,并有着和类型一样的名字。

如果一个类型定义了一个方法,就使用它;如果不是,就使用匿名成员定义的方法。如果两个匿名成员都提供一个方法,编译器会报错,但只在该方法被调用的情况下。

这种组合是通过委派来实现的,而不是继承。一旦匿名成员的方法被调用,控制流就被委托给了该方法。


type Base struct {}
func (Base) Magic() { fmt.Print("base magic") }
func (self Base) MoreMagic() {
  self.Magic()
  self.Magic()
}

type Foo struct {
  Base
}
func (Foo) Magic() { fmt.Print("foo magic") }

// 将得到:
f := new(Foo)
f.Magic() //=> foo magic
f.MoreMagic() //=> base magic base magic

要注意两种继承的不同之处
type NewStudent Student // 不会继承 Student 的方法
type SubStudent struct { Student } // 会继承 Student 的方法

并发

并行是关于性能的;并发是关于程序设计的。

goroutine

goroutine是一个普通的函数,只是需要使用保留字go作为开头。
goroutine有简单的模型:它是与其他goroutine并行执行的,有着相同地址空间的函数。是轻量的,仅比分配栈空间多一点点消耗,并且随着需要在堆空间上分配(和释放)。

虽然goroutine是并发执行的,但是它们并不是并行执行的。如果不告诉Go额外的东西,同一时刻只会有一个goroutine执行。利用runtime.GOMAXPROCS(n)可以设置goroutine并行执行的数量。来自文档:GOMAXPROCS设置了同时运行的CPU的最大数量,并返回之前的设置。如果n<1,不会改变当前设置。当调度得到改进后,这将被移除。

runtime与goroutine

runtime包有几个处理goroutine的函数:

  • Goexit:退出当前执行的goroutine,但是defer函数还是会继续调用。
  • Gosched:让出当前goroutine的执行权限,掉赌气安排其他等待中的任务运行,并在下次某个时候从该位置恢复执行。(相当于Java的Thread.yield()
  • NumCPU:返回CPU核数量。
  • NumGoroutine:返回正在执行和排队的任务总数。
  • GOMAXPROCS:用来设置可以运行的CPU核数。

channel

channel可以与Unix shell中的双向管道做类比:可以通过它发送或者接收值,这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。

必须使用make创建channel,如果创建时不指定大小,将创建无缓冲的channel。无缓冲意味着:读取时将被阻塞,直到有数据可接收;发送也将被阻塞,直到数据被读出。无缓冲channel可在多个goroutine之间同步。
go-channel

创建: ci := make(chan int); ci := make(chan int, 4) // 4是channel的缓冲大小。
发送到channel: ci <- 1
从channel接收: i := <- ci

range和close

for i := range c的形式能够不断地从channel c读取数据,直到c被显式关闭。

关闭channel之后就无法再发送任何数据了,在消费方可以通过语法v, ok := <-ch测试channel是否被关闭。如果ok返回false,那么说明channel已经没有任何数据并且已经被关闭。

记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic

另外记住一点的就是channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的

select

select可以监听多个channel上的数据流动。

select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。

在select里面还有default语法,select其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。

超时

可以利用select来设置超时,通过如下的方式实现:


func main() {
    c := make(chan int)
    o := make(chan bool)
    go func() {
        for {
            select {
                case v := <- c:
                    println(v)
                case <- time.After(5 * time.Second):
                    println("timeout")
                    o <- true
                    break
            }
        }
    }()
    <- o
}

欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据