Go 函数是“一等公民”的理解

九路
• 阅读 2193

函数(function)作为现代编程语言的基本语法元素存在于支持各种范式(paradigm)的主流编程语言当中。无论是命令式语言 C、多范式通用编程语言 C++,还是面向对象编程语言 Java、Ruby,亦或是函数式语言 Haskell、动态脚本语言 Python、PHP、JavaScript,函数这一语法元素都是当仁不让的核心。

Go 语言以“成为新一代系统级语言”为目标而诞生,但在演化过程中,逐渐演化成了面向并发、契合现代硬件发展趋势的通用编程语言。Go 语言中没有那些典型的面向对象语言的语法,比如类、继承、对象等。Go 语言中的方法(method)本质上亦是函数的一个“变种”。因此,在 Go 语言中,函数是唯一一种基于特定输入、实现特定任务并可反馈任务执行结果的代码块。本质上我们可以说 Go 程序就是一组函数的集合。

和其他编程语言中的函数相比,Go 语言的函数具有如下特点:

  • 以“func”关键字开头;
  • 支持多返回值;
  • 支持具名返回值;
  • 支持递归调用;
  • 支持同类型的可变参数;
  • 支持 defer,实现函数优雅返回

更为关键的是函数在 Go 语言中属于“一等公民(first-class citizen)”。众所周知,并不是在所有编程语言中函数都是“一等公民”,本节中我就和大家一起来看看成为”一等公民“的函数都有哪些特质可以帮助我们写出优雅简洁的代码。

1. 什么是“一等公民”

关于什么是编程语言的“一等公民”,业界并没有教科书给出精准的定义。这里引用一下 wiki 发明人、C2 站点作者沃德·坎宁安(Ward Cunningham)对“一等公民”的诠释:

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为函数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。在动态类型语言中,语言运行时还支持对“一等公民”类型的检查。

基于上面关于“一等公民”的诠释,我们来看看 Go 语言的函数是如何满足上述条件而成为“一等公民”的。

1.1 正常创建

我们可以在源码顶层正常创建一个函数,如下面的函数 newPrinter:

// $GOROOT/src/fmt/print.go
func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.wrapErrs = false
    p.fmt.init(&p.buf)
    return p
}

1.2 在函数内创建

在 Go 语言中,我们可以在函数内定义一个新函数,如下面代码中在 hexdumpWords 函数内部定义的匿名函数(被赋值给变量 p1)。在 C/C++中我们无法实现这一点,这也是 C/C++语言中函数不是“一等公民”的原因之一。

// $GOROOT/src/runtime/print.go
func hexdumpWords(p, end uintptr, mark func(uintptr) byte) {
        p1 := func(x uintptr) {
                var buf [2 * sys.PtrSize]byte
                for i := len(buf) - 1; i >= 0; i-- {
                        if x&0xF < 10 {
                                buf[i] = byte(x&0xF) + '0'
                        } else {
                                buf[i] = byte(x&0xF) - 10 + 'a'
                        }
                        x >>= 4
                }
                gwrite(buf[:])
        }
    ... ...
}

1.3 作为类型

我们可以使用函数来自定义类型,如下面代码中的 HandlerFunc、visitFunc 和 action:

// $GOROOT/src/net/http/server.go
type HandlerFunc func(ResponseWriter, *Request)

// $GOROOT/src/sort/genzfunc.go
type visitFunc func(ast.Node) ast.Visitor

// codewalk: https://tip.golang.org/doc/codewalk/functions/
type action func(current score) (result score, turnIsOver bool)

1.4 存储到变量中

我们可以将定义好的函数存储到一个变量中,如下面代码中的 apply:

// $GOROOT/src/runtime/vdso_linux.go
func vdsoParseSymbols(info *vdsoInfo, version int32) {
        if !info.valid {
                return
        }

        apply := func(symIndex uint32, k vdsoSymbolKey) bool {
                sym := &info.symtab[symIndex]
                typ := _ELF_ST_TYPE(sym.st_info)
                bind := _ELF_ST_BIND(sym.st_info)

        ... ...

                *k.ptr = info.loadOffset + uintptr(sym.st_value)
                return true
        }
    ... ...
}

1.5 作为参数传入函数

我们可以将函数作为参数传入函数,比如下面代码中函数 AfterFunc 的参数 f:

$GOROOT/src/time/sleep.go

func AfterFunc(d Duration, f func()) *Timer {
        t := &Timer{
                r: runtimeTimer{
                        when: when(d),
                        f:    goFunc,
                        arg:  f,
                },
        }
        startTimer(&t.r)
        return t
}

1.6 作为返回值从函数返回

函数还可以被作为返回值从函数返回,如下面代码中函数 makeCutsetFunc 的返回值就是一个函数:

// $GOROOT/src/strings/strings.go
func makeCutsetFunc(cutset string) func(rune) bool {
        if len(cutset) == 1 && cutset[0] < utf8.RuneSelf {
                return func(r rune) bool {
                        return r == rune(cutset[0])
                }
        }
        if as, isASCII := makeASCIISet(cutset); isASCII {
                return func(r rune) bool {
                        return r < utf8.RuneSelf && as.contains(byte(r))
                }
        }
        return func(r rune) bool { return IndexRune(cutset, r) >= 0 }
}

我们看到:就像沃德·坎宁安(Ward Cunningham)对“一等公民”的诠释中所说的那样,Go 中的函数可以像普通整型值那样被创建和使用。

除了上面那些例子,函数还可以被放入数组/切片/map 等结构中、可以像其他类型变量一样被赋值给 interface{}、甚至我们可以建立元素为函数的 channel,如下面例子:

// function_as_first_class_citizen_1.go
package main

import "fmt"

type binaryCalcFunc func(int, int) int

func main() {
    var i interface{} = binaryCalcFunc(func(x, y int) int { return x + y })
    c := make(chan func(int, int) int, 10)
    fns := []binaryCalcFunc{
        func(x, y int) int { return x + y },
        func(x, y int) int { return x - y },
        func(x, y int) int { return x * y },
        func(x, y int) int { return x / y },
        func(x, y int) int { return x % y },
    }

    c <- func(x, y int) int {
        return x * y
    }

    fmt.Println(fns[0](5, 6))
    f := <-c
    fmt.Println(f(7, 10))
    v, ok := i.(binaryCalcFunc)
    if !ok {
        fmt.Println("type assertion error")
        return
    }

    fmt.Println(v(17, 7))
}

和 C/C++这类语言相比,作为“一等公民”的 Go 函数拥有难得的"灵活性"。接下来我们需要考虑如何使用 Go 函数才能发挥出它作为“一等公民”的最大效用。

2. 函数作为“一等公民”的特殊运用

1). 像整型变量那样对函数进行显式转型

Go 是类型安全的语言,Go 语言不允许隐式类型转换,因此下面的代码是无法通过编译的:

var a int = 5
var b int32 = 6
fmt.Println(a + b) // 违法操作: a + b (不匹配的类型int和int32)

我们必须通过对上面代码进行显式的转型才能通过编译器的检查:

var a int = 5
var b int32 = 6
fmt.Println(a + int(b)) // ok。输出11

函数是“一等公民”,对整型变量进行的操作也同样可以用在函数上面,即函数也可以被显式转型,并且这样的转型在特定的领域具有奇妙的作用。一个最为典型的示例就是 http.HandlerFunc 这个类型,我们来看一下例子:

// function_as_first_class_citizen_2.go
package main

import (
        "fmt"
        "net/http"
)

func greeting(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Welcome, Gopher!\n")
}                    

func main() {
        http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}

上述代码是我们日常最为常见的一个用 Go 构建的 Web Server 的例子。其工作机制很简单,当用户通过浏览器或类似 curl 这样的命令行工具访问 Web server 的 8080 端口时,会收到“Welcome, Gopher!”一行文字版应答。很多 Gopher 可能并未真正深入分析过这段代码,这里用到的恰恰是函数作为“一等公民”的特性,我们来看一下。

我们先来看一下 ListenAndServe 的源码:

// $GOROOT/src/net/http/server.go
func ListenAndServe(addr string, handler Handler) error {
        server := &Server{Addr: addr, Handler: handler}
        return server.ListenAndServe()
}

ListenAndServe 会将来自客户端的 http 请求交给其第二个参数 handler 处理,而这里 handler 参数的类型 http.Handler 接口:

// $GOROOT/src/net/http/server.go
type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

该接口仅有一个方法:ServeHTTP,其原型为:func(http.ResponseWriter, *http.Request)。这与我们自己定义的 http 请求处理函数 greeting 的原型是一致的。但是我们没法直接将 greeting 作为参数值传入,否则会报下面错误:

func(http.ResponseWriter, *http.Request) does not implement http.Handler (missing ServeHTTP method)

即函数 greeting 并未实现接口 Handler 的方法,无法将其赋值给 Handler 类型的参数。

在代码中我们也并未直接将 greeting 传入 ListenAndServe,而是将 http.HandlerFunc(greeting)作为参数传给了 ListenAndServe。我们来看看 http.HandlerFunc 是什么?

// $GOROOT/src/net/http/server.go

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        f(w, r)
}

HandlerFunc 其实就是一个基于函数定义的新类型,它的底层类型为 func(ResponseWriter, *Request)。该类型有一个方法 ServeHTTP,继而实现了 Handler 接口。也就是说 http.HandlerFunc(greeting)这句代码的真正含义是将函数 greeting 显式转换为 HandlerFunc 类型,后者实现了 Handler 接口,满足 ListenAndServe 函数第二个参数的要求。

另外,之所以 http.HandlerFunc(greeting)这个语句可以通过编译器检查,正是因为 HandlerFunc 的底层类型正是 func(ResponseWriter, *Request),与 greeting 的原型是一致的。这和下面整型变量的转型原理并无二致:

type MyInt int
var x int = 5
y := MyInt(x) // MyInt的底层类型为int,类比 HandlerFunc的底层类型为func(ResponseWriter, *Request)

为了充分理解这种显式转型的“技巧”,我们再来看一个简化后的例子:

// function_as_first_class_citizen_3.go
package main

import "fmt"

type BinaryAdder interface {
    Add(int, int) int
}

type MyAdderFunc func(int, int) int

func (f MyAdderFunc) Add(x, y int) int {
    return f(x, y)
}

func MyAdd(x, y int) int {
    return x + y
}

func main() {
    var i BinaryAdder = MyAdderFunc(MyAdd)
    fmt.Println(i.Add(5, 6))
}

和 Web server 那个例子一样,我们想将 MyAdd 这个函数赋值给 BinaryAdder 这个接口,直接赋值是不行的,我们需要一个底层函数类型与 MyAdd 一致的自定义类型的显式转换,这个自定义类型就是 MyAdderFunc,该类型实现了 BinaryAdder 接口,这样在经过 MyAdderFunc 的显式转型后,MyAdd 被赋值给了 BinaryAdder 的变量 i。这样通过 i 调用的 Add 方法实质上就是我们的 MyAdd 函数。

2) 函数式编程

Go 语言演进到如今,对多种编程范式或多或少都有支持。比如:对函数式编程的支持就得意于函数是“一等公民”的特质。虽然 Go 不推崇函数式编程,但有些时候应用一些函数式编程风格可以写出更加优雅、更简洁、更易维护的代码。

柯里化函数

我们先来看一种函数式编程的典型应用:柯里化函数(currying)。在计算机科学中,柯里化是把接受多个参数的函数变换成接受一个单一参数(原函数的第一个参数)的函数,并且返回接受余下的参数和返回结果的新函数的技术。这个技术以逻辑学家 Haskell Curry 命名。

定义总是拗口难懂,我们来用 Go 编写一个直观的柯里化函数的例子:

// function_as_first_class_citizen_4.go
package main

import "fmt"

func times(x, y int) int {
    return x * y
}

func partialTimes(x int) func(int) int {
    return func(y int) int {
        return times(x, y)
    }
}

func main() {
    timesTwo := partialTimes(2)
    timesThree := partialTimes(3)
    timesFour := partialTimes(4)
    fmt.Println(timesTwo(5))
    fmt.Println(timesThree(5))
    fmt.Println(timesFour(5))
}

运行这个例子:

$ go run function_as_first_class_citizen_4.go
10
15
20

这里的柯里化是指将原接受两个参数的函数 times 转换为接受一个参数的 partialTimes 的过程。通过 partialTimes 函数构造的 timesTwo 将输入参数扩大为原先 2 倍、timesThree 将输入参数扩大为原先的 3 倍…。

这个例子利用了函数的几点性质:

  • 在函数中定义,通过返回值返回
  • 闭包

闭包是前面没有提到的 Go 函数支持的一个特性。 闭包是在函数内部定义的匿名函数,并且允许该匿名函数访问定义它的外部函数的作用域。本质上,闭包是将函数内部和函数外部连接起来的桥梁。

以上述示例来说,partialTimes 内部定义的匿名函数就是一个闭包,该匿名函数访问了其外部函数(partialTimes)的变量 x。这样当调用 partialTimes(2)时,partialTimes 实际上返回一个调用 times(2,y)的函数:

timesTwo = func(y int) int {
        return times(2, y)
    }

函子(Functor)

函数式编程范式最让人”望而却步“的就是首先要了解一些抽象概念,比如上面的柯里化,再比如这里的函子(Functor)。什么是函子呢?具体来说,成为函子需要两个条件:

  • 函子本身是一个容器类型,以 Go 语言为例,这个容器可以是切片、map 甚至是 channel;
  • 光是容器还不够,该容器类型还需要实现一个方法,该方法接受一个函数类型参数,并在容器的每个元素上应用那个函数,得到一个新的函子,原函子容器内部的元素值不受到影响

我们还是用一个具体的示例来直观看一下吧:

// function_as_first_class_citizen_5.go
package main

import (
    "fmt"
)

type IntSliceFunctor interface {
    Fmap(fn func(int) int) IntSliceFunctor
}

type intSliceFunctorImpl struct {
    ints []int
}

func (isf intSliceFunctorImpl) Fmap(fn func(int) int) IntSliceFunctor {
    newInts := make([]int, len(isf.ints))
    for i, elt := range isf.ints {
        retInt := fn(elt)
        newInts[i] = retInt
    }
    return intSliceFunctorImpl{ints: newInts}
}

func NewIntSliceFunctor(slice []int) IntSliceFunctor {
    return intSliceFunctorImpl{ints: slice}
}

func main() {
    // 原切片
    intSlice := []int{1, 2, 3, 4}
    fmt.Printf("init a functor from int slice: %#v\n", intSlice)
    f := NewIntSliceFunctor(intSlice)
    fmt.Printf("original functor: %+v\n", f)

    mapperFunc1 := func(i int) int {
        return i + 10
    }

    mapped1 := f.Fmap(mapperFunc1)
    fmt.Printf("mapped functor1: %+v\n", mapped1)

    mapperFunc2 := func(i int) int {
        return i * 3
    }
    mapped2 := mapped1.Fmap(mapperFunc2)
    fmt.Printf("mapped functor2: %+v\n", mapped2)
    fmt.Printf("original functor: %+v\n", f) // 原functor没有改变
    fmt.Printf("composite functor: %+v\n", f.Fmap(mapperFunc1).Fmap(mapperFunc2))
}

运行这段代码:

$ go run function_as_first_class_citizen_5.go
init a functor from int slice: []int{1, 2, 3, 4}
original functor: {ints:[1 2 3 4]}
mapped functor1: {ints:[11 12 13 14]}
mapped functor2: {ints:[33 36 39 42]}
original functor: {ints:[1 2 3 4]}
composite functor: {ints:[33 36 39 42]}

分析这段代码:

  • 这里我们定义了一个 intSliceFunctorImpl 类型,用来作为 functor 的载体;
  • 我们把 functor 要实现的方法命名为 Fmap,intSliceFunctorImpl 类型实现了该方法。同时该方法也是 IntSliceFunctor 接口的唯一方法;可以看到在这个代码中真正的 functor 其实是 IntSliceFunctor,这符合 Go 的惯用法;
  • 我们定义了创建 IntSliceFunctor 的函数:NewIntSliceFunctor。通过该函数以及一个初始切片,我们可以实例化一个 functor;
  • 我们在 main 中定义了两个转换函数,并将这两个函数应用到上述 functor 实例;我们看到得到的新 functor 的内部容器元素值是在原容器的元素值经由转换函数转换后得到的;
  • 在最后,我们还可以对最初的 functor 实例连续(组合)应用转换函数,这让我们想到了数学课程中的函数组合;
  • 无论如何应用转换函数,原 functor 中容器内的元素值不受到影响。

functor 非常适合对容器集合元素做批量同构处理,而且代码也要比每次都对容器中的元素作循环处理要优雅简洁许多。但要想在 Go 中发挥 functor 的最大效能,还需要 Go 对泛型提供支持,否则我们就需要为每一种容器类型都实现一套对应的 Functor 机制。比如上面的示例仅支持元素类型为 int 的切片,如果元素类型换为 string 或元素类型依然为 int,但容器类型换为 map,我们还需要分别为之编写新的配套代码。

延续传递式(Continuation-passing Style)

函数式编程离不开递归,以求阶乘函数为例,我们可以轻易用递归方法写出一个实现:

// function_as_first_class_citizen_6.go
func factorial(n int) int {
        if n == 1 {
                return 1
        } else {
                return n * factorial(n-1)
        }
}

func main() {
    fmt.Printf("%d\n", factorial(5))
}

这是一个非常常规的求阶乘的实现思路,但是这种思路并未应用到函数作为”一等公民“的任何特质。函数式编程有一种被称为延续传递式(Continuation-passing Style,以下简称为 CPS)的编程风格可以充分运用函数作为”一等公民“的特质。

在 CPS 风格中,函数是不允许有返回值的。一个函数 A 应该将其想返回的值显式传给一个 continuation 函数(一般接受一个参数),而这个 continuation 函数自身是函数 A 的一个参数。概念太过抽象,我们用一个简单的例子来说明一下:

下面得 Max 函数的功能是返回两个参数值中较大的那个值:

// function_as_first_class_citizen_7.go
package main

import "fmt"

func Max(n int, m int) int {
        if n > m {
                return n
        } else {
                return m
        }
}

func main() {
        fmt.Printf("%d\n", Max(5, 6))
}

我们把 Max 函数看作是上面定义中的 A 函数在未 CPS 化之前的状态。接下来,我们来根据 CPS 的定义将其转换为 CPS 风格:

  • 首先我们去掉 Max 函数的返回值,并为其添加一个函数类型的参数 f(这个 f 就是定义中的 continuation 函数)
func Max(n int, m int, f func(int))

将返回结果传给 continuation 函数,即把 return 语句替换为对 f 函数的调用

func Max(n int, m int, f func(int)) {
        if n > m {
                f(n)
        } else {
                f(m)
        }
}

完整的转换后的代码如下:

// function_as_first_class_citizen_8.go
package main

import "fmt"

func Max(n int, m int, f func(y int)) { 
        if n > m { 
                f(n)
        } else { 
                f(m)
        } 

}

func main() { 
        Max(5, 6, func(y int) { fmt.Printf("%d\n", y) })
}

接下来,我们使用同样的方法将上面的阶乘实现转换为 CPS 风格。

  • 首先我们去掉 factorial 函数的返回值,并为其添加一个函数类型的参数 f(这个 f 也就是 CPS 定义中的 continuation 函数)
func factorial(n int, f func(y int))
  • 接下来,将 factorial 实现中的返回结果传给 continuation 函数,即把 return 语句替换为对 f 函数的调用
func factorial(n int, f func(int)) {
    if n == 1 {
        f(n)
    } else {
        factorial(n-1, func(y int) { f(n * y) })
    }
}

由于原 else 分支有递归,因此我们需要把未完成的计算过程封装为一个新的函数 f’作为 factorial 递归调用的第二个参数,f’的参数 y 即为原 factorial(n-1)的计算结果,而 n * y 是要传递给 f 的,于是 f’这个函数的定义就为:func(y int) { f(n * y) }。

转换为 CPS 风格的阶乘函数的完整代码如下:

// function_as_first_class_citizen_9.go
package main

import "fmt"

func factorial(n int, f func(int)) {
    if n == 1 {
        f(1) //基本情况
    } else {
        factorial(n-1, func(y int) { f(n * y) })
    }
}

func main() {
    factorial(5, func(y int) { fmt.Printf("%d\n", y) })
}

我们简单解析一下上述实例代码的执行过程(下面用伪代码阐释):

f1 = func(y int) { fmt.Printf("%v\n", y) 
factorial(5, f1)

f2 = func(y int) {f1(5 * y)}
factorial(4, f2)

f3 = func(y int) {f2(4 * y)}
factorial(3, f3)

f4 = func(y int) {f3(3 * y)}
factorial(2, f4)

f5 = func(y int) {f4(2 * y)}
factorial(1, f5)

f5(1)

=> 

f5(1) 
= f4(2 * 1)
= f3(3 * 2 * 1)
= f2(4 * 3 * 2 * 1)
= f1(5 * 4 * 3 * 2 * 1) 
= 120

读到这里很多朋友会提出心中的疑问:这种 CPS 风格虽然利用了函数作为”一等公民“的特质,但是其代码理解起来颇为困难,这种风格真的好吗?朋友们的担心是有道理的。这里对 CPS 风格的讲解其实是一个”反例“,目的就是告诉大家,尽管作为”一等公民“的函数给 Go 带来的强大的表达能力,但是如果选择了不适合的风格或者说为了函数式而进行函数式编程,那么就会出现代码难于理解,且代码执行效率不高的情况(CPS 需要语言支持尾递归优化,但 Go 目前并不支持)。

3. 小结

成为”一等公民“的函数极大增强了 Go 语言的表现力,我们可以像对待值变量那样对待函数,上述函数编程思想的运用就得益于此。

让自己习惯于函数是”一等公民“,请牢记本节要点:

  • Go 函数可以像变量值那样被赋值给变量、作为参数传递、作为返回值返回和在函数内部创建等;
  • 函数可以像变量那样被显式转型;
  • 基于函数特质,了解 Go 中的几种有用的函数式编程风格:柯里化、函子;
  • 不要为了符合特定风格而滥用函数特质。
点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Souleigh ✨ Souleigh ✨
3年前
Swift 简介
Swift和ObjectiveC的主要区别1,编程范式Swift可以面向协议编程、函数式编程、面向对象编程。Swift语言引入了协议、协议的扩展、泛型等新特性,因此使用Swift语言可以很好地面向协议编程;Swift语言将函数和闭包提升为语言的一等公民,函数可以作为一个变量、可以作为其他函数的参数、作为其他函数的返回值等来传递,所以
Stella981 Stella981
2年前
Scala学习(一):Scala简介与Hello World!
一.Scala是什么Scala是一门多范式的编程语言,类似于Java。设计初衷是实现可伸缩的语言、并集成面向对象编程和函数式编程的各种特性。二.环境准备(以Windows下安装Scala为例)1.官网:http://www.scalalang.org/ 下载安装包:!image(https://static.osc
Stella981 Stella981
2年前
Rust开发环境搭建
1.Rust概述按照百度百科的说法,Rust是一门系统编程语言,专注于安全,尤其是并发安全,支持函数式和命令式以及泛型等编程范式的多范式语言。Rust在语法上和C类似,但是设计者想要在保证性能的同时提供更好的内存安全。Rust最初是由Mozilla研究院的GraydonHoare设计创造,然后在DaveHerman,Brend
Stella981 Stella981
2年前
JavaScript的入门简介
什么是JavaScriptJavaScript,我们一般简称为JS,是一种具有函数优先的轻量级,解释型或即时编译型的编程语言。JavaScript现在已经被用到了很多非浏览器环境中,JavaScript基于原型编程、多范式的动态脚本语言,并支持面向对象、命令式和声明式风格。HTML、CSS、JavaScript三者不同的功能:
Stella981 Stella981
2年前
JS 对象数组Array 根据对象object key的值排序sort,很风骚哦
有个js对象数组varary\{id:1,name:"b"},{id:2,name:"b"}\需求是根据name或者id的值来排序,这里有个风骚的函数函数定义:function keysrt(key,desc) {  return function(a,b){    return desc ? ~~(ak
Stella981 Stella981
2年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
2年前
00_设计模式之语言选择
设计模式之语言选择设计模式简介背景设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。设计模式(Designpattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的
Python进阶者 Python进阶者
2个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这