精华内容
下载资源
问答
  • 函数方法和接口

    千次阅读 2019-08-24 16:50:58
    Go语言中的函数有具名和匿名之分:具名函数一般对应于包级的函数,是匿名函数的一特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数函数式编程语言的核心。方法是绑定到一个具体类型的特殊...

    8.25打卡学习记录

    1.4 函数、方法和接口

    函数对应操作序列,是程序的基本组成元素。Go语言中的函数有具名和匿名之分:具名函数一般对应于包级的函数,是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。方法是绑定到一个具体类型的特殊函数,Go语言中的方法是依托于类型的,必须在编译时静态绑定。接口定义了方法的集合,这些方法依托于运行时的接口对象,因此接口对应的方法是在运行时动态绑定的。Go语言通过隐式接口机制实现了面向对象模型

    Go语言程序的初始化和执行总是从main.main函数开始的。但是如果main包导入了其它的包,则会按照顺序将它们包含进main包里(这里的导入顺序依赖具体实现,一般可能是以文件名或包路径名的字符串顺序导入)。如果某个包被多次导入的话,在执行的时候只会导入一次。当一个包被导入时,如果它还导入了其它的包,则先将其它的包包含进来,然后创建和初始化这个包的常量和变量,再调用包里的init函数,如果一个包有多个init函数的话,调用顺序未定义(实现可能是以文件名的顺序调用),同一个文件内的多个init则是以出现的顺序依次调用(init不是普通函数,可以定义有多个,所以也不能被其它函数调·)。最后,当main包的所有包级常量、变量被创建和初始化完成,并且init函数被执行后,才会进入main.main函数,程序开始正常执行。下图是Go程序函数启动顺序的示意图:

    图 1-11 包初始化流程

    要注意的是,在main.main函数执行之前所有代码都运行在同一个goroutine,也就是程序的主系统线程中。因此,如果某个init函数内部用go关键字启动了新的goroutine的话,新的goroutine只有在进入main.main函数之后才可能被执行到。

    1.4.1 函数

    在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例。当然,Go语言中每个类型还可以有自己的方法,方法其实也是函数的一种。

    // 具名函数
    func Add(a, b int) int {
    	return a+b
    }
    
    // 匿名函数
    var Add = func(a, b int) int {
    	return a+b
    }
    

    Go语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数

    // 多个参数和多个返回值
    func Swap(a, b int) (int, int) {
    	return b, a
    }
    
    // 可变数量的参数
    // more 对应 []int 切片类型
    func Sum(a int, more ...int) int {
    	for _, v := range more {
    		a += v
    	}
    	return a
    }
    

    当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果:

    func main() {
    	var a = []interface{}{123, "abc"}
    
    	Print(a...) // 123 abc
    	Print(a)    // [123 abc]
    }
    
    func Print(a ...interface{}) {
    	fmt.Println(a...)
    }
    

    第一个Print调用时传入的参数是a...,等价于直接调用Print(123, "abc")。第二个Print调用传入的是未解包的a,等价于直接调用Print([]interface{}{123, "abc"})

    不仅函数的参数可以有名字,也可以给函数的返回值命名:

    func Find(m map[int]int, key int) (value int, ok bool) {
    	value, ok = m[key]
    	return
    }
    

    如果返回值命名了,可以通过名字来修改返回值,也可以通过defer语句在return语句之后修改返回值:

    func Inc() (v int) {
    	defer func(){ v++ } ()
    	return 42
    }
    

    其中defer语句延迟执行了一个匿名函·,因为这个匿名函数捕获了外部函数的局部变量v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问

    闭包的这种引用方式访问外部变量的行为可能会导致一些隐含的问题:

    func main() {
    	for i := 0; i < 3; i++ {
    		defer func(){ println(i) } ()
    	}
    }
    // Output:
    // 3
    // 3
    // 3
    

    因为是闭包,在for迭代语句中,每个defer语句延迟执行的函数引用的都是同一个i迭代变量,在循环结束后这个变量的值为3,因此最终输出的都是3。

    修复的思路是在每轮迭代中为每个defer函数生成独有的变量。可以用下面两种方式:

    func main() {
    	for i := 0; i < 3; i++ {
    		i := i // 定义一个循环体内局部变量i
    		defer func(){ println(i) } ()
    	}
    }
    
    func main() {
    	for i := 0; i < 3; i++ {
    		// 通过函数传入i
    		// defer 语句会马上对调用参数求值
    		defer func(i int){ println(i) } (i)
    	}
    }
    

    第一种方法是在循环体内部再定义一个局部变量,这样每次迭代defer语句的闭包函数捕获的都是不同的变量,这些变量的值对应迭代时的值。第二种方式是将迭代变量通过闭包函数的参数传入,defer语句会马上对调用参数求值。两种方式都是可以工作的。不过一般来说,在for循环内部执行defer语句并不是一个好的习惯,此处仅为示例,不建议使用。

    Go语言中,如果以切片为参数调用函数时,有时候会给人一种参数采用了传引用的方式的假象:因为在被调用函数内部可以修改传入的切片的元素。其实,任何可以通过函数参数修改调用参数的情形,都是因为函数参数中显式或隐式传入了指针参数。函数参数传值的规范更准确说是只针对数据结构中固定的部分传值,例如字符串或切片对应结构体中的指针和字符串长度结构体传值,但是并不包含指针间接指向的内容。将切片类型的参数替换为类似reflect.SliceHeader结构体就很好理解切片传值的含义了:

    func twice(x []int) {
    	for i := range x {
    		x[i] *= 2
    	}
    }
    
    type IntSliceHeader struct {
    	Data []int
    	Len  int
    	Cap  int
    }
    
    func twice(x IntSliceHeader) {
    	for i := 0; i < x.Len; i++ {
    		x.Data[i] *= 2
    	}
    }
    

    因为切片中的底层数组部分是通过隐式指针传递(指针本身依然是传值的,但是指针指向的却是同一份的数据),所以被调用函数是可以通过指针修改掉调用参数切片中的数据。除了数据之外,切片结构还包含了切片长度和切片容量信息,这2个信息也是传值的。如果被调用函数中修改了LenCap信息的话,就无法反映到调用参数的切片中,这时候我们一般会通过返回修改后的切片来更新之前的切片。这也是为何内置的append必须要返回一个切片的原因。

    Go语言中,函数还可以直接或间接地调用自己,也就是支持递归调用。Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。每个goroutine刚启动时只会分配很小的栈(4或8KB,具体依赖实现),根据需要动态调整栈的大小,栈最大可以达到GB级(依赖具体实现,在目前的实现中,32位体系结构为250MB,64位体系结构为1GB)。在Go1.4以前,Go的动态栈采用的是分段式的动态栈,通俗地说就是采用一个链表来实现动态栈,每个链表的节点内存位置不会发生变化。但是链表实现的动态栈对某些导致跨越链表不同节点的热点调用的性能影响较大,因为相邻的链表节点它们在内存位置一般不是相邻的,这会增加CPU高速缓存命中失败的几率。为了解决热点调用的CPU缓存命中率问题,Go1.4之后改用连续的动态栈实现,也就是采用一个类似动态数组的结构来表示栈。不过连续动态栈也带来了新的问题:当连续栈动态增长时,需要将之前的数据移动到新的内存空间,这会导致之前栈中全部变量的地址发生变化。虽然Go语言运行时会自动更新引用了地址变化的栈变量的指针,但最重要的一点是要明白Go语言中指针不再是固定不变的了(因此不能随意将指针保持到数值变量中,Go语言的地址也不能随意保存到不在GC控制的环境中,因此使用CGO时不能在C语言中长期持有Go语言对象的地址)。

    因为,Go语言函数的栈会自动调整大小,所以普通Go程序员已经很少需要关心栈的运行机制的。在Go语言规范中甚至故意没有讲到栈和堆的概念。我们无法知道函数参数或局部变量到底是保存在栈中还是堆中,我们只需要知道它们能够正常工作就可以了。看看下面这个例子:

    func f(x int) *int {
    	return &x
    }
    
    func g() int {
    	x = new(int)
    	return *x
    }
    

    第一个函数直接返回了函数参数变量的地址——这似乎是不可以的,因为如果参数变量在栈上的话,函数返回之后栈变量就失效了,返回的地址自然也应该失效了。但是Go语言的编译器和运行时比我们聪明的多,它会保证指针指向的变量在合适的地方。第二个函数,内部虽然调用new函数创建了*int类型的指针对象,但是依然不知道它具体保存在哪里。对于有C/C++编程经验的程序员需要强调的是:不用关心Go语言中函数栈和堆的问题,编译器和运行时会帮我们搞定;同样不要假设变量在内存中的位置是固定不变的,指针随时可能会变化,特别是在你不期望它变化的时候。

    1.4.2 方法

    方法一般是面向对象编程(OOP)的一个特性,在C++语言中方法对应一个类对象的成员函数,是关联到具体对象上的虚表中的。但是Go语言的方法却是关联到类型的,这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。面向对象编程(OOP)进入主流开发领域一般认为是从C++开始的,C++就是在兼容C语言的基础之上支持了class等面向对象的特性。然后Java编程则号称是纯粹的面向对象语言,因为Java中函数是不能独立存在的,每个函数都必然是属于某个类的。

    面向对象编程更多的只是一种思想,很多号称支持面向对象编程的语言只是将经常用到的特性内置到语言中了而已。Go语言的祖先C语言虽然不是一个支持面向对象的语言,但是C语言的标准库中的File相关的函数也用到了的面向对象编程的思想。下面我们实现一组C语言风格的File函数:

    // 文件对象
    type File struct {
    	fd int
    }
    
    // 打开文件
    func OpenFile(name string) (f *File, err error) {
    	// ...
    }
    
    // 关闭文件
    func CloseFile(f *File) error {
    	// ...
    }
    
    // 读文件数据
    func ReadFile(f *File, offset int64, data []byte) int {
    	// ...
    }
    

    其中OpenFile类似构造函数用于打开文件对象,CloseFile类似析构函数用于关闭文件对象,ReadFile则类似普通的成员函数,这三个函数都是普通的函数。CloseFileReadFile作为普通函数,需要占用包级空间中的名字资源。不过CloseFileReadFile函数只是针对File类型对象的操作,这时候我们更希望这类函数和操作对象的类型紧密绑定在一起。

    Go语言中的做法是,将CloseFileReadFile函数的第一个参数移动到函数名的开头:

    // 关闭文件
    func (f *File) CloseFile() error {
    	// ...
    }
    
    // 读文件数据
    func (f *File) ReadFile(offset int64, data []byte) int {
    	// ...
    }
    

    这样的话,CloseFileReadFile函数就成了File类型独有的方法了(而不是File对象方法)。它们也不再占用包级空间中的名字资源,同时File类型已经明确了它们操作对象,因此方法名字一般简化为CloseRead

    // 关闭文件
    func (f *File) Close() error {
    	// ...
    }
    
    // 读文件数据
    func (f *File) Read(offset int64, data []byte) int {
    	// ...
    }
    

    将第一个函数参数移动到函数前面,从代码角度看虽然只是一个小的改动,但是从编程哲学角度来看,Go语言已经是进入面向对象语言的行列了。我们可以给任何自定义类型添加一个或多个方法。每种类型对应的方法必须和类型的定义在同一个包中,因此是无法给int这类内置类型添加方法的(因为方法的定义和类型的定义不在一个包中)。对于给定的类型,每个方法的名字必须是唯一的,同时方法和函数一样也不支持重载。

    方法是由函数演变而来,只是将函数的第一个对象参数移动到了函数名前面了而已。因此我们依然可以按照原始的过程式思维来使用方法。通过叫方法表达式的特性可以将方法还原为普通类型的函数:

    // 不依赖具体的文件对象
    // func CloseFile(f *File) error
    var CloseFile = (*File).Close
    
    // 不依赖具体的文件对象
    // func ReadFile(f *File, offset int64, data []byte) int
    var ReadFile = (*File).Read
    
    // 文件处理
    f, _ := OpenFile("foo.dat")
    ReadFile(f, 0, data)
    CloseFile(f)
    

    在有些场景更关心一组相似的操作:比如Read读取一些数组,然后调用Close关闭。此时的环境中,用户并不关心操作对象的类型,只要能满足通用的ReadClose行为就可以了。不过在方法表达式中,因为得到的ReadFileCloseFile函数参数中含有File这个特有的类型参数,这使得File相关的方法无法和其它不是File类型但是有着相同ReadClose方法的对象无缝适配。这种小困难难不倒我们Go语言码农,我们可以通过结合闭包特性来消除方法表达式中第一个参数类型的差异:

    // 先打开文件对象
    f, _ := OpenFile("foo.dat")
    
    // 绑定到了 f 对象
    // func Close() error
    var Close = func() error {
    	return (*File).Close(f)
    }
    
    // 绑定到了 f 对象
    // func Read(offset int64, data []byte) int
    var Read = func(offset int64, data []byte) int {
    	return (*File).Read(f, offset, data)
    }
    
    // 文件处理
    Read(0, data)
    Close()
    

    这刚好是方法值也要解决的问题。我们用方法值特性可以简化实现:

    // 先打开文件对象
    f, _ := OpenFile("foo.dat")
    
    // 方法值: 绑定到了 f 对象
    // func Close() error
    var Close = f.Close
    
    // 方法值: 绑定到了 f 对象
    // func Read(offset int64, data []byte) int
    var Read = f.Read
    
    // 文件处理
    Read(0, data)
    Close()
    

    Go语言不支持传统面向对象中的继承特性,而是以自己特有的组合方式支持了方法的继承。Go语言中,通过在结构体内置匿名的成员来实现继承:

    import "image/color"
    
    type Point struct{ X, Y float64 }
    
    type ColoredPoint struct {
    	Point
    	Color color.RGBA
    }
    

    虽然我们可以将ColoredPoint定义为一个有三个字段的扁平结构的结构体,但是我们这里将Point嵌入到ColoredPoint来提供XY这两个字段。

    var cp ColoredPoint
    cp.X = 1
    fmt.Println(cp.Point.X) // "1"
    cp.Point.Y = 2
    fmt.Println(cp.Y)       // "2"
    

    通过嵌入匿名的成员,我们不仅可以继承匿名成员的内部成员,而且可以继承匿名成员类型所对应的方法。我们一般会将Point看作基类,把ColoredPoint看作是它的继承类或子类。不过这种方式继承的方法并不能实现C++中虚函数的多态特性。所有继承来的方法的接收者参数依然是那个匿名成员本身,而不是当前的变量。

    type Cache struct {
    	m map[string]string
    	sync.Mutex
    }
    
    func (p *Cache) Lookup(key string) string {
    	p.Lock()
    	defer p.Unlock()
    
    	return p.m[key]
    }
    

    Cache结构体类型通过嵌入一个匿名的sync.Mutex来继承它的LockUnlock方法. 但是在调用p.Lock()p.Unlock()时, p并不是LockUnlock方法的真正接收者, 而是会将它们展开为p.Mutex.Lock()p.Mutex.Unlock()调用. 这种展开是编译期完成的, 并没有运行时代价.

    在传统的面向对象语言(eg.C++或Java)的继承中,子类的方法是在运行时动态绑定到对象的,因此基类实现的某些方法看到的this可能不是基类类型对应的对象,这个特性会导致基类方法运行的不确定性。而在Go语言通过嵌入匿名的成员来“继承”的基类方法,this就是实现该方法的类型的对象,Go语言中方法是编译时静态绑定的。如果需要虚函数的多态特性,我们需要借助Go语言接口来实现。

    1.4.3 接口

    Go语言之父Rob Pike曾说过一句名言:那些试图避免白痴行为的语言最终自己变成了白痴语言(Languages that try to disallow idiocy become themselves idiotic)。一般静态编程语言都有着严格的类型系统,这使得编译器可以深入检查程序员有没有作出什么出格的举动。但是,过于严格的类型系统却会使得编程太过繁琐,让程序员把大好的青春都浪费在了和编译器的斗争中。Go语言试图让程序员能在安全和灵活的编程之间取得一个平衡。它在提供严格的类型检查的同时,通过接口类型实现了对鸭子类型的支持,使得安全动态的编程变得相对容易。

    Go的接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。Go语言中的面向对象就是如此,如果一个对象只要看起来像是某种接口类型的实现,那么它就可以作为该接口类型使用。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不用去破坏这些类型原有的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其灵活有用。Go语言的接口类型是延迟绑定,可以实现类似虚函数的多态功能。

    接口在Go语言中无处不在,在“Hello world”的例子中,fmt.Printf函数的设计就是完全基于接口的,它的真正功能由fmt.Fprintf函数完成。用于表示错误的error类型更是内置的接口类型。在C语言中,printf只能将几种有限的基础数据类型打印到文件对象中。但是Go语言灵活接口特性,fmt.Fprintf却可以向任何自定义的输出流对象打印,可以打印到文件或标准输出、也可以打印到网络、甚至可以打印到一个压缩文件;同时,打印的数据也不仅仅局限于语言内置的基础类型,任意隐式满足fmt.Stringer接口的对象都可以打印,不满足fmt.Stringer接口的依然可以通过反射的技术打印。fmt.Fprintf函数的签名如下:

    func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
    

    其中io.Writer用于输出的接口,error是内置的错误接口,它们的定义如下:

    type io.Writer interface {
    	Write(p []byte) (n int, err error)
    }
    
    type error interface {
    	Error() string
    }
    

    我们可以通过定制自己的输出对象,将每个字符转为大写字符后输出:

    type UpperWriter struct {
    	io.Writer
    }
    
    func (p *UpperWriter) Write(data []byte) (n int, err error) {
    	return p.Writer.Write(bytes.ToUpper(data))
    }
    
    func main() {
    	fmt.Fprintln(&UpperWriter{os.Stdout}, "hello, world")
    }
    

    当然,我们也可以定义自己的打印格式来实现将每个字符转为大写字符后输出的效果。对于每个要打印的对象,如果满足了fmt.Stringer接口,则默认使用对象的String方法返回的结果打印:

    type UpperString string
    
    func (s UpperString) String() string {
    	return strings.ToUpper(string(s))
    }
    
    type fmt.Stringer interface {
    	String() string
    }
    
    func main() {
    	fmt.Fprintln(os.Stdout, UpperString("hello, world"))
    }
    

    Go语言中,对于基础类型(非接口类型)不支持隐式的转换,我们无法将一个int类型的值直接赋值给int64类型的变量,也无法将int类型的值赋值给底层是int类型的新定义命名类型的变量。Go语言对基础类型的类型一致性要求可谓是非常的严格,但是Go语言对于接口类型的转换则非常的灵活。对象和接口之间的转换、接口和接口之间的转换都可能是隐式的转换。可以看下面的例子:

    var (
    	a io.ReadCloser = (*os.File)(f) // 隐式转换, *os.File 满足 io.ReadCloser 接口
    	b io.Reader     = a             // 隐式转换, io.ReadCloser 满足 io.Reader 接口
    	c io.Closer     = a             // 隐式转换, io.ReadCloser 满足 io.Closer 接口
    	d io.Reader     = c.(io.Reader) // 显式转换, io.Closer 不满足 io.Reader 接口
    )
    

    有时候对象和接口之间太灵活了,导致我们需要人为地限制这种无意之间的适配。常见的做法是定义一个含特殊方法来区分接口。比如runtime包中的Error接口就定义了一个特有的RuntimeError方法,用于避免其它类型无意中适配了该接口:

    type runtime.Error interface {
    	error
    
    	// RuntimeError is a no-op function but
    	// serves to distinguish types that are run time
    	// errors from ordinary errors: a type is a
    	// run time error if it has a RuntimeError method.
    	RuntimeError()
    }
    

    在protobuf中,Message接口也采用了类似的方法,也定义了一个特有的ProtoMessage,用于避免其它类型无意中适配了该接口:

    type proto.Message interface {
    	Reset()
    	String() string
    	ProtoMessage()
    }
    

    不过这种做法只是君子协定,如果有人刻意伪造一个proto.Message接口也是很容易的。再严格一点的做法是给接口定义一个私有方法。只有满足了这个私有方法的对象才可能满足这个接口,而私有方法的名字是包含包的绝对路径名的,因此只能在包内部实现这个私有方法才能满足这个接口。测试包中的testing.TB接口就是采用类似的技术:

    type testing.TB interface {
    	Error(args ...interface{})
    	Errorf(format string, args ...interface{})
    	...
    
    	// A private method to prevent users implementing the
    	// interface and so future additions to it will not
    	// violate Go 1 compatibility.
    	private()
    }
    

    不过这种通过私有方法禁止外部对象实现接口的做法也是有代价的:首先是这个接口只能包内部使用,外部包正常情况下是无法直接创建满足该接口对象的;其次,这种防护措施也不是绝对的,恶意的用户依然可以绕过这种保护机制。

    在前面的方法一节中我们讲到,通过在结构体中嵌入匿名类型成员,可以继承匿名类型的方法。其实这个被嵌入的匿名成员不一定是普通类型,也可以是接口类型。我们可以通过嵌入匿名的testing.TB接口来伪造私有的private方法,因为接口方法是延迟绑定,编译时private方法是否真的存在并不重要。

    package main
    
    import (
    	"fmt"
    	"testing"
    )
    
    type TB struct {
    	testing.TB
    }
    
    func (p *TB) Fatal(args ...interface{}) {
    	fmt.Println("TB.Fatal disabled!")
    }
    
    func main() {
    	var tb testing.TB = new(TB)
    	tb.Fatal("Hello, playground")
    }
    

    我们在自己的TB结构体类型中重新实现了Fatal方法,然后通过将对象隐式转换为testing.TB接口类型(因为内嵌了匿名的testing.TB对象,因此是满足testing.TB接口的),然后通过testing.TB接口来调用我们自己的Fatal方法。

    这种通过嵌入匿名接口或嵌入匿名指针对象来实现继承的做法其实是一种纯虚继承,我们继承的只是接口指定的规范,真正的实现在运行的时候才被注入。比如,我们可以模拟实现一个gRPC的插件:

    type grpcPlugin struct {
    	*generator.Generator
    }
    
    func (p *grpcPlugin) Name() string { return "grpc" }
    
    func (p *grpcPlugin) Init(g *generator.Generator) {
    	p.Generator = g
    }
    
    func (p *grpcPlugin) GenerateImports(file *generator.FileDescriptor) {
    	if len(file.Service) == 0 {
    		return
    	}
    
    	p.P(`import "google.golang.org/grpc"`)
    	// ...
    }
    

    构造的grpcPlugin类型对象必须满足generate.Plugin接口(在"github.com/golang/protobuf/protoc-gen-go/generator"包中):

    type Plugin interface {
    	// Name identifies the plugin.
    	Name() string
    	// Init is called once after data structures are built but before
    	// code generation begins.
    	Init(g *Generator)
    	// Generate produces the code generated by the plugin for this file,
    	// except for the imports, by calling the generator's methods
    	// P, In, and Out.
    	Generate(file *FileDescriptor)
    	// GenerateImports produces the import declarations for this file.
    	// It is called after Generate.
    	GenerateImports(file *FileDescriptor)
    }
    

    generate.Plugin接口对应的grpcPlugin类型的GenerateImports方法中使用的p.P(...)函数却是通过Init函数注入的generator.Generator对象实现。这里的generator.Generator对应一个具体类型,但是如果generator.Generator是接口类型的话我们甚至可以传入直接的实现。

    Go语言通过几种简单特性的组合,就轻易就实现了鸭子面向对象和虚拟继承等高级特性,真的是不可思议。

    感谢Go语言圣经这个只为记录学习!!!

    展开全文
  • 极限、连续与求极限的方法·求极限的方法概述(约12):一、极限的概念与性质(一)极限的定义(二)极限的性质()两个重要极限二、极限存在性的判别(一)极限存在的两个准则(二)极限存在的一个充要条件(...


    第一章 极限、连续与求极限的方法

    在这里插入图片描述

    ·求极限的方法概述(约12种):

    1. 利用极限的四则运算与幂指数运算法则
    2. 利用函数的连续性
    3. 利用变量替换与两个重要极限
    4. 利用等价无穷小因子替换
    5. 利用洛必达法则
    6. 分别求左右极限
    7. 数列极限转化为函数极限
    8. 利用适当放大缩小法
    9. 对递归数列先证明极限存在(常用到单调有界数列有极限的准则,对于无单调性有界数列还要用其他方法),再利用递推关系求出极限
    10. 利用导数的定义求极限
    11. 利用泰勒公式
    12. 利用定积分求n项式和的极限
    13. 利用拉格朗日中值定理求极限

    一、极限的概念与性质

    (一)极限的定义

    1.1 数列的极限
    1.2 函数的极限,趋近无穷时的极限
    注:xn趋向的含义不同,前者有正负,后者只有正
    1.3 函数的极限,趋近于x0时的左右极限

    (二)极限的性质

    1.1 数列极限的不等式性质(两条)
    1.2 收敛数列的有界性
    1.3 函数极限的不等式性质(两条)
    推论:极限的保号性
    1.4 存在极限的函数的局部有界性

    (三)两个重要极限

    二、极限存在性的判别

    (一)极限存在的两个准则

    1.5 数列夹逼定理
    1.6 函数夹逼定理
    1.7 单调有界数列必收敛定理

    (二)极限存在的一个充要条件

    1.8 函数极限存在的充要条件,分段函数在分段点的左右极限相等
    1.9 数列极限存在的充要条件,偶数项极限 = 奇数项极限 = A <=> 数列极限 = A
    (所有子数列的极限都相等)

    (三)证明函数极限不存在的常用方法

    方法1:左右极限不相等,(比如含有那三个函数的极限要对正负无穷分别求极限,比如开根号、取绝对值时存在的正负问题)
    方法2:xnyn趋近于x0f(xn)f(yn)的极限不相等 (例1.4的Ⅰ)
    方法3:不存在 + 存在 = 不存在、不存在 × 存在 = 不存在 (运算法则)(例1.4的Ⅱ)


    三、求极限的方法

    (一)利用极限四则运算和幂指数运算法则求极限

    1.10 极限的四则运算法则及其推广
    1.11 幂指数函数的极限运算法则及其推广
    注:只有 每部分的极限存在才可用四则运算法则

    (二)利用函数的连续性求极限
    1. 代入法
    2. 一切初等函数在定义域内都连续
    (三)利用变量替换法与两个重要极限求极限

    主要是1的无穷型极限
    注意看变量是否真的趋近于0,有可能变量极限不存在

    (四)利用等价无穷小因子替换求极限

    记住大概11个等价无穷小

    (五)洛必达

    洛就完事了

    (六)分别求左右极限

    要提高警觉,注意有哪些会导致左右不一致的变量

    (七)利用函数极限求数列极限

    主要是为了利用洛必达法则

    (八)利用放缩法

    利用夹逼定理
    掌握几种放缩手段,对分子分母进行调整,极限不等式,积分的极限,积分不等式等等

    (九)递归数列极限的求法

    方法1:先证数列收敛,然后去解
    方法2:利用两个结论

    (十)利用导数的定义求极限(见第二章)
    (十一)利用定积分求某些n项式和的极限(见第三章)
    (十二)利用泰勒公式求未定式的极限(见第五章)

    四、无穷小及其比较

    1. 无穷小阶的比较,分式,常用洛必达或者泰勒公式
      在这里插入图片描述

    2. 确定无穷小的阶的方法
      方法1:等价无穷小
      方法2:待定阶数法
      方法3:泰勒公式(见第五章)
      方法4:利用无穷小阶的运算性质


    五、函数的连续性及其判断

    (一)连续性及其相关概念

    1.8 连续性的定义((1)~(3)有三个互相等价的定义)
    (4)~(6)左连续、右连续、内连续

    (二)间断点的定义与分类

    1.9 间断点的定义

    1. 第一类
    2. 第二类
    (三)判断函数的连续性和间断点的类型
    1. 初等函数
    2. 连续性运算法则
    3. 定义
    4. 分别判断左右连续性
      1.14 连续性运算法则
    5. 两个函数做四则运算
    6. 两个函数做复合运算
    7. 反函数连续性

    六、连续函数的性质

    (一)连续函数的局部保号性质
    (二)有界闭区间上连续函数的性质

    1.16 有界闭区间上连续函数的有界性
    推论:第一类间断点 => 有界
    1.17 有界闭区间上连续函数存在最大、最小值
    1.18 连续函数介值定理
    推论:连续函数零点存在性定理
    注:①推广到开区间;②有界闭区间;③存在一点使得

    推论:根据最大值最小值得出函数值域
    注:求连续函数值域,就是求连续函数最值

    (三)方程式根到存在性——连续介值定理的应用

    可用来证明 f(x) = 0有根

    这章常考题型约有十二种


    后记:

    对于上面的第13条,利用拉格朗日中值定理求极限,例题(法三):
    例题

    法1,法2
    法3


    常用等价无穷小


    回到顶部

    展开全文
  • 关于定义域有界性的三种判断

    万次阅读 2016-12-19 19:57:32
    关于定义域有界性的三种判断@(微积分)给定一个函数,讨论其在定义域上是否有界,有三种方法。不敢说常见,提出来思考。 理论法:若f(x)在定义域[a,b]上连续,或者放宽到常义可积(有限个第一类间断点),则f(x)在[a,...

    关于定义域有界性的三种判断

    @(微积分)

    给定一个函数,讨论其在定义域上是否有界,有三种方法。不敢说常见,提出来思考。

    • 理论法:若f(x)在定义域[a,b]上连续,或者放宽到常义可积(有限个第一类间断点),则f(x)在[a,b]上必然有界。
    • 计算法:切分

      • (a,b)内连续
      • limxa+f(x)
      • limxbf(x)
        则f(x)在定义域[a,b]内有界。
    • 运算规则判定:在边界极限不存在时

      • 有界函数 ± 有界函数 = 有界函数 (有限个,基本不会有无穷个,无穷是个难分高低的状态)
      • 有界 x 有界 = 有界

    这是三种看似没什么用的结论,但是用起来才能明白它的效用。

    举个例子:

    讨论函数 f(x)=(x31)sinx(x2+1)|x| 在其定义域上的有界性。

    分析:这种看着也挺简单的,对吧。

    从这个函数中可以看出,定义域是 (,0)(0,+)

    分成两段,那么问题将转化为四个极限的求解。

    limxf(x)
    limx+f(x)
    limx0+f(x)
    limx0f(x)

    如果四个极限存在,则可说明f(x)有界。

    分别计算:

    limxf(x)=limx(x31)sinx(x2+1)|x|=limx(x31)(x2+1)(x)sinx

    大概可以一眼看出是两个有界函数之积了。因此极限存在。
    同理可得:

    limx+f(x)=limx+(x31)sinx(x2+1)|x|=limx(x31)(x2+1)xsinx

    也是极限存在。

    limx0f(x)=limx0(x31)sinx(x2+1)|x|=limx0(x31)(x2+1)sinxx=1

    limx0+f(x)=limx0+(x31)sinx(x2+1)|x|=limx0+(x31)(x2+1)sinxx=1

    当变元趋近某一个值时,代入不会出现分母为0,不必犹豫,能代入则代入。

    这样,四个极限都存在,就可以说明函数在定义域内有界了。

    展开全文
  • 高数函数连续性与间断点

    万次阅读 多人点赞 2016-11-30 16:19:57
    这几天做的真题中涉及到的函数连续性和间断点的题也不少,而且... 所谓连续,顾名思义,下面有两定义方法:  (1)    该定义主要是用于证明题,考查逻辑推理问题。  (2)设函数f(x)在点X0的某一领域内有

        这几天做的真题中涉及到的函数的连续性和间断点的题也不少,而且正确率不高,下面总结一下这部分知识。

        【知识点】

        一、连续性

         所谓连续,顾名思义,下面有两种定义方法:

         (1)

            

          该定义主要是用于证明题,考查逻辑推理问题。

         (2)设函数f(x)在点X0的某一领域内有定义,且有x->时,f(x)的极限等于f(X0),则称函数f(x)在点X0处是连续的。

            

         这个定义用的最多,最广泛。根据这个定义我们可以知道,如果给出一个函数是,在某一点处有定义,则可以推出改点的极限等于函数在改点的值。

        二、间断点

         1、定义

           

         判读函数间断点(不连续)有三种情形:

           

         2、常见类型

          间断点的常见类型有:无穷间断点、振荡间断点、可去间断点和跳跃间断点

            1)、无穷间断点:函数在x0处没有定义,并且左右极限都不存在。

                 

                                                                                                         

                                       

            2)、振荡间断点:函数在x0处没有定义,并且在x->x0时,函数值变动无限次,我们就称x0为函数的振荡间断点。

            

                                  

           3)、可去间断点:函数在x0处没有定义,存在左右极限,且左右极限相等,我们就称x0为函数的可去间断点。

            

           4)、跳跃间断点:函数在x0处有定义,存在左右极限,但左右极限不相等,因函数在x0处产生跳跃现象,我们就称x0为函数的跳跃间断点。

                 

                               

        3、分类

          通常把间断点分成两类:

          第一类间断点:某点是函数的间断点,该点的左右极限都存在,则称改点是函数的第一类间断点。如果左右极限相等称为“可去间断点”,不相等称为“跳跃间断点”。

          第二类间断点:不是第一类间断点的任何间断点,也可以说左右极限至少有一个不存在的点,“无穷间断点”和“振荡间断点”就是第二类。

                         

              

        【小结】

         函数的连续性和间断点的判断需要借助函数的极限,所有的知识都是有联系的,如果想求出一道数学题,可能需要联系很多个知识点 ,还是多多做题,积累做题技巧吧!

    展开全文
  • 函数可导,其导函数是否一定连续”?这个问题的答案是,不一定连续。有些同学我估计审题就审错了,把这个问题看成了“可导是否一定连续”。排除开这种粗心大意的情况,这个问题还是有点反直觉。首先,看着函数研究...
  • 两个无穷小的乘积仍是无穷小,而两个无穷小之商却有如下几情况: 例如:当时,、、都是无穷小,但是 ,, 两个无穷小之比的极限的各种不同情况, 反映出不同无穷小趋向于零时,在“快慢”上是有区别的。 由...
  • 判断互质的五种方法

    千次阅读 2017-05-01 16:25:30
    判断互质数的五种方法 一. 概念判断法 公约数只有1的两个数叫做互质数。根据互质数的概念可以对一组数是否互质进行判断。如:9和11的公约数只有1,则它们是互质数。 二. 规律判断法 根据互质数的...
  • 如果excel函数中的函数被广泛使用,尤其是在单条件判断中,那么很好地使用if函数可以帮助我们完成许多函数.最简单的excel if函数应用程序示例: 下图中的数据在d列中显示以下结果: 如果数据1大于60,则显示为合格,...
  • 在高等数学一元函数微分学中研究的关键问题之一是可导和可微,夹杂着函数连续,简短等知识点,这几个相关的概念混在一块总是难以理解,什么可导一定可微,可导一定连续之类的。 这里把这几个概念就自己的理解做一下...
  • 本文介绍了无界函数反常积分的比较审敛法和极限审敛法,以及特殊的无界函数Γ函数,以及Γ函数的一些特殊属性。
  • Sql练习--查询连续出现次的数据

    千次阅读 2019-08-03 23:44:45
    编写一个 SQL 查询,查找所有至少连续出现次的数字。 +----+-----+ | Id | Num | +----+-----+ | 1 | 1 | | 2 | 1 | | 3 | 1 | | 4 | 2 | | 5 | 1 | | 6 | 2 | | 7 | 2 | +----+-----+ 例如,给定上面的 Logs ...
  • 单片机处理按键长按的三种解决方法

    千次阅读 多人点赞 2021-01-06 21:03:12
    首先,我们在判断按键按下时,最简单的处理方法是直接比较按键对应引口的高低电平。 比如 按键按下点亮LED,我们可以直接写 //假设button1代表按键1,led低电平点亮 if(button1==0) { led=0; //亮 } else if...
  • 函数参数的默认值

    千次阅读 2018-01-21 21:59:47
    ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。 function log(x, y) { y = y || 'World'; console.log(x, y);}log('Hello') // Hello Worldlog('Hello', 'China') // Hello Chinalog('Hello', ''...
  • ZEMAX 中三种设计优化方法

    千次阅读 多人点赞 2019-11-13 21:50:35
    ZEMAX提供的优化方法三种:Local、Gloal、Hammer Optimization 1) Local Optimization 这种优化方法强烈依赖初始结构,系统初始结构通常也被称为系统的起点,在这一起点处优化驱使评价函数逐渐降低,直至到最低...
  • 人工智能(一现代的方法)(第版)(学习笔记)第一部分 人工智能第1章 绪论第2章 智能 AgentAgent通过传感器感知环境并通过执行器对所处环境产生影响。理性Agent的定义理性的判断依赖理性与全知的区别理性Agent...
  • 最长公共子序列(LCS)问题有两方式定义子序列,一是子序列不要求不连续,一是子序列必须连续。上一章介绍了用两算法解决子序列不要求连续的最终公共子序列问题,本章将介绍要求子序列必须是连续的情况下...
  • 状态机实现的三种方法-C语言

    万次阅读 多人点赞 2017-05-12 15:40:49
    1. 参考:... 2. ... 有限状态机FSM思想广泛应用于硬件控制电路设计,也是软件上常用的一处理方法(软件上称为FMM有限消息机)。它把复杂的控制逻辑分解成有限个稳定状态,在每个状态上
  • 这一节主要学习凸函数的定义以及性质。了解保凸运算,以及上镜图与下水平集等。这些基础知识看似零乱,然而却是后面的基础。...水平集是以函数的形式表示集合,类似于等高线,在历史上是重要的方法。这里我们通...
  • §3.4 函数的单调性 一、从几何图形上看函数的单调性 运行matlab程序gs0303.m,可得到函数与它的导函数在上的图象,从图形上可以观察到: 函数在上是单调减少,在上是单调增加; 其导函数在上小于零,在上...
  • 三元函数(三元函数能几何表示吗)

    千次阅读 2021-02-05 02:49:37
    三元函数可是用二元函数来表示比方说f(x,y,z)=g(x,y)+g(y,z)+g(x,z),但是二元函数是在平面坐标系中表现的,而三元函数就是维坐标系,这样看在维坐标系中画一个向.那么三元函数表示有什么几何空间意义呢,那么四...
  • GELU激活函数

    千次阅读 2021-11-12 18:21:47
    GELU激活函数为xΦ(x)xΦ(x)xΦ(x),其中Φ(x)Φ(x)Φ(x)为标准高斯累积分布函数。GELU非线性根据输入的值来权重,而不是像ReLUs(x1x>0x1_{x>0}x1x>0​)那样通过符号来门输入。本文对ReLU和ELU激活的G.
  • 最长公共子序列(LCS)问题有两方式定义子序列,一是子序列不要求不连续,一是子序列必须连续。上一章介绍了用两算法解决子序列不要求连续的最终公共子序列问题,本章将介绍要求子序列必须是连续的情况下...
  • Linux字符设备驱动注册三种方法以及内核分析

    千次阅读 多人点赞 2018-10-07 15:59:38
    其中最多的是字符设备,其中字符设备的注册方法主要有三种:杂项设备注册、早期字符设备注册、标准字符设备注册。以及详细介绍各类方法注册。 开发环境: PC:VMworkstation 12 运行Ubuntu12 32位虚拟机 开发板:...
  • 图像清晰度评价15种方法对比

    千次阅读 2020-01-06 21:05:17
    本文针对无参考图像质量评价应用,对目前几较为常用的、具有代表性清晰度算法进行讨论分析,为实际应用中选择清晰度算法提供依据。 (1)Brenner 梯度函数 Brenner梯度函数是最简单的梯度评价函数,它只是简单...
  • 三种方法求n个数的最大公约数

    万次阅读 2017-03-22 18:31:05
     Function List: // 主要函数及其功能  int input(int t[]); //输入函数  int gcd(int a,int b); //2个数求最大公约数  int exper(int t[],int n);//验证函数  Gcd(int t[],int n)   ...
  • C++定义函数

    千次阅读 2019-07-16 15:26:07
    一、函数传递参数的方式 术语: 主调函数:调用其他函数函数,大部分时候为main函数 被调函数:被其他函数如main函数调用的函数 变元:在主调函数中传递给被调函数的变量或常量,如函数调用语句function(a,b)...
  • 判别函数(七)势函数

    千次阅读 2018-01-18 23:50:07
    假设要划分属于两类别ω1和ω2的模式样本,这些样本可看成是分布在n维模式空间中的点xk。把属于ω1的点比拟为某种能源点,在点上,电位达到峰值。随着与该点距离的增大,电位分布迅速减小,即把样本xk附近空间x点...
  • 还有另一种方法就是研究需要极小化的目标函数。如果这个函数是凸函数,那么就可以确保该函数的局部极小值即是全局最小值 定理 :凸函数的局部极小值就是全局最小值 ( Proof page 9 ) 2.凸函数 2.1 什么...
  • 问题描述 假设国家发行了n不同面值的...例如,当n=5和m=4时,面值为(1,3,11,15,32)的5邮票可以贴出邮资的最大连续邮资区间是1-70。 问题分析 对于连续邮资问题,用n元组x[1:n]表示n不同的邮票面值,并约定它...
  • 16常用的数据分析方法汇总

    万次阅读 多人点赞 2017-04-04 16:16:33
    经常会有朋友问到一个朋友,数据分析常用的分析方法有哪些,我需要学习哪个等等之类的问题,今天数据分析精选给大家整理了十六常用的数据分析方法,供大家参考学习。 一、描述统计 描述性统计是指运用制表和...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 150,574
精华内容 60,229
关键字:

判断函数连续的三种方法

友情链接: GPURayTracerSrc.zip