go基础(二)

运算符

  • 算数运算符
  • 关系运算符
  • 逻辑运算符
  • 位运算符
  • 赋值运算符
  • 其他运算符
    ps:为防止发生混淆,go语法规定,++ --运算均为语句而非一个运算符

流程控制

条件语句

  • if/if else语句
  • switch语句
    • fallthrough
      case后面增添fallthrough,可以在执行完这条语句后继续执行下一个case。

if/if else和switch语句后面都可以执行一个简单的语句,然后再加上判断语句,但是有这个语句定义的变量的作用域仅在该代码块范围之内。
例:

package main

import (
    "fmt"
    "math"
)

func pow(x, n, lim float64) float64  {
    if v := math.Pow(x, n); v < lim {
        return v
    }
    return lim
}
func main(){
    fmt.Println(
        pow(3, 2, 10),
        pow(3, 3, 20),
    )
}
  • select
    select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

循环语句

对于go来说,只有一种循环,for循环。即go没有while循环。
倒是for在go里面是十分强大的,既可以用来循环读取数据,又可以当作C语言的while来控制逻辑,还可以进行迭代操作。
基本语法:

for init; condition; post { }

省略init和post,for就可以当作while来使用:

func main() {
    a := 1
    for a < 1000 {
        a += a
    }
    fmt.Println(a)
}

循环控制语句

  • range
    for 语句使用 range 子句可以迭代出一个数组或切片值中的每个元素,一个字符串值中的每个字符或者一个字典值中的每个键值对
ints := []int{1, 2, 3, 4, 5}
for i, d := range ints {
    fmt.Printf("%d: %d\n", i, d)
}
  • break
  • continue
  • goto
  • defer
    /* GO语言特有的一个流程控制语句,它用来预定对一个函数的调用。它只能出现在一个函数中(假设是A函数),且只能调用另一个函数(假设是B函数),意味着在A函数结束返回时,延迟调用B函数,一般用于打开文件时的资源清理等工作。如果一个函数内部调用多个 defer 语句,则遵循后进先出的原则。defer 语句后面可以跟着匿名函数,来快速实现一些临时的功能。defer 调用的函数可以使用的变量,可以是通过参数传进来的,也可以是上下文中可以调用的变量,如果是传参进来的,则会立即被求值,如果是上下文中的变量,则不会立即被求值,而是取在 defer 函数调用时的值,这一点要注意。*/
  • 异常处理语句

函数

关键字func用于定义函数。Go中的函数有些不太方便的限制,但也借鉴了动态语言的一些优点:

  • 无需前置声明
  • 不支持命名嵌套
  • 不支持同命名函数重载
  • 不支持默认参数
  • 支持不定长变参
  • 支持多返回类型
  • 支持命名返回
  • 支持闭包

函数定义格式

func function_name( parameter list )  return_types  {  //花括号不能另起一行
   函数体
}

函数属于第一类对象,具备相同签名(参数及返回值列表)的视作同一类型。

func hello() {
    println("hello,world!")
}
func exec(f func()) {
    f()
}
func main() {
    f:=hello
    exec(f)
}

ps:第一类对象(first-class object)指可在运行期创建,可用作函数参数或返回值,可存入变量的实体。最常见的用法就是匿名函数。

值传递

传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

函数只能判断其是否为nil,不支持其他比较操作。

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int = 200

   fmt.Printf("交换前 a 的值为 : %d\n", a )    //100
   fmt.Printf("交换前 b 的值为 : %d\n", b )    //100

   /* 通过调用函数来交换值 */
   swap(a, b)

   fmt.Printf("交换后 a 的值 : %d\n", a )   //100
   fmt.Printf("交换后 b 的值 : %d\n", b )    //100
}

/* 定义相互交换值的函数 */
func swap(x, y int) int {
   var temp int

   temp = x /* 保存 x 的值 */
   x = y    /* 将 y 值赋给 x */
   y = temp /* 将 temp 值赋给 y*/

   return temp;
}

程序中使用的是值传递, 所以两个值并没有实现交互。

引用传递

引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。


import "fmt"

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int= 200

   fmt.Printf("交换前,a 的值 : %d\n", a )    //100
   fmt.Printf("交换前,b 的值 : %d\n", b )    //200

   /* 调用 swap() 函数
   * &a 指向 a 指针,a 变量的地址
   * &b 指向 b 指针,b 变量的地址
   */
   swap(&a, &b)

   fmt.Printf("交换后,a 的值 : %d\n", a )    //200
   fmt.Printf("交换后,b 的值 : %d\n", b )    //100
}

func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保存 x 地址上的值 */
   *x = *y      /* 将 y 值赋给 x */
   *y = temp    /* 将 temp 值赋给 y */
}

变参

变参本质上就是一个切片。只能接收一到多个同类型参数,且必须放在列表尾部。

func test(s string,a...int) {
    fmt.Printf("%T, %v\n",a,a) // 显示类型和值
}
func main() {
    test("abc",1,2,3,4)        //[]int, [1 2 3 4]
}

返回值

有返回值的函数,必须有明确的return终止语句:

func test(x int) int {
    if x > 0{
        return 1
    } else if x < 0 {
        return -1
    }
          //error: missing return at the end of function
}

如果有panic或者是无break的死循环,可以没有return终止语句。
go语言借鉴了动态语言的多返回值模型,函数得以返回更多状态,尤其是error模式(go的错误处理模式):

import"errors"
func div(x,y int) (int,error) {        // 多返回值列表必须使用括号
    if y==0{
        return 0,errors.New("division by zero")
    }
    return x/y,nil
}

多返回值可用作其他函数调用实参,或当作结果直接返回。

func div(x,y int) (int,error) {
    if y==0{
        return 0,errors.New("division by zero")
    }
    return x/y,nil
}
func log(x int,err error) {
    fmt.Println(x,err)
}
func test() (int,error) {
    return div(5,0)      // 多返回值用作return结果
}
func main() {
    log(test())       // 多返回值用作实参
}

命名返回值

命名返回值和参数一样,可当作函数局部变量使用,最后由return隐式返回。

func div(x,y int) (z int,err error) {
    if y==0{
        err=errors.New("division by zero")
        return
    }
    z=x/y
    return       // 相当于"return z,err"
}

匿名函数

匿名函数是指没有定义名字符号的函数。
除没有名字外,匿名函数和普通函数完全相同。最大区别是,可在函数内部定义匿名函数,形成类似嵌套效果。匿名函数可直接调用,保存到变量,作为参数或返回值。

  • 直接执行
func main() {
    func(s string) {
        println(s)
    }("hello,world!")
}
  • 赋值给变量
func main() {
    add:=func(x,y int)int{
        return x+y
    }
    println(add(1,2))
}
  • 作为参数
func test(f func()) {
    f()
}
func main() {
    test(func() {
        println("hello,world!")
    })
}
  • 作为返回值
func test()func(int,int)int{
    return func(x,y int)int{
        return x+y
    }
}
func maidn() {
    add:=test()
    println(add(1,2))
}

闭包

Go 语言支持匿名函数,可作为闭包。是函数和其引用的环境的组合体。

package main

import "fmt"

func getSequence() func() int {
   i:=0
   return func() int {
      i+=1
     return i  
   }
}

func main(){
   /* nextNumber 为一个函数,函数 i 为 0 */
   nextNumber := getSequence()  

   /* 调用 nextNumber 函数,i 变量自增 1 并返回 */
   fmt.Println(nextNumber())    //1
   fmt.Println(nextNumber())    //2
   fmt.Println(nextNumber())    //3
   
   /* 创建新的函数 nextNumber1,并查看结果 */
   nextNumber1 := getSequence()  
   fmt.Println(nextNumber1())    //1
   fmt.Println(nextNumber1())    //2
}

闭包让我们不用传递参数就可读取或修改环境状态,当然也要为此付出额外代价。
因为闭包通过指针引用环境变量,那么可能会导致其生命周期延长,甚至被分配到堆内存。在对于性能要求较高的场合应当慎用。

延迟调用

语句defer向当前函数注册稍后执行的函数调用。这些调用被称作延迟调用,因为它们直到当前函数执行结束前才被执行,常用于资源释放、解除锁定,以及错误处理等操作。

func main() {
    f,err:=os.Open("./main.go")
    if err!=nil{
        log.Fatalln(err)
    }
    defer f.Close()    // 仅注册,直到main退出前才执行
    
 //   ...do something...

}

多个延迟注册按FILO次序执行。

func main() {
    defer println("a")
    defer println("b")
}

output:
b
a

go风格的错误处理(浓浓复古风)

  • error
    官方推荐的标准做法是返回error状态。
func Scanln(a...interface{}) (n nit, err error)

标准库将error定义为接口类型,以便实现自定义错误类型。

type error interface{
    Error() string
}        //接口类型下面会详细介绍
  • panic, recover
    与error相比,panic/recover在使用方法上更接近try/catch结构化异常。
func  panic(v interface{})
func  recover()interface{}

它们是内置函数而非语句。panic会立即中断当前函数流程,执行延迟调用。
而在延迟调用函数中,recover可捕获并返回panic提交的错误对象。

func main() {
    defer func() {
        if err:=recover();err!=nil{ // 捕获错误
            log.Fatalln(err)
        }
    }()
    panic("i am dead") // 引发错误
    println("exit.") // 永不会执行
}

因为panic参数是空接口类型,因此可使用任何对象作为错误状态。而recover返回结果同样要做转型才能获得具体信息。
无论是否执行recover,所有延迟调用都会被执行。但中断性错误会沿调用堆栈向外传递,要么被外层捕获,要么导致进程崩溃。

func test() {
    defer println("test.1")
    defer println("test.2")
    panic("i am dead")
}
func main() {
    defer func() {
        log.Println(recover())
    }()
    test()
}

output:
test.2
test.1
i am dead

方法

方法是与对象实例绑定的特殊函数。
方法和函数定义语法区别的在于前者有前置实例接收参数(receiver),编译器以此确定方法所属类型。
可以为当前包,以及除接口和指针以外的任何类型定义方法。
在Go语言中,我们并不会像其它语言那样用this或者self作为接收器;我们可以任意的选择接收器的名字。

package geometry

import "math"

type Point struct{ X, Y float64 }

// traditional function
func Distance(p, q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

// same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
    return math.Hypot(q.X-p.X, q.Y-p.Y)
}

上面的代码里那个附加的参数p,叫做方法的接收器(receiver)。在方法调用过程中,接收器参数一般会在方法名之前出现。这和方法声明是一样的,都是接收器参数在方法名字之前。下面是例子:

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // "5", function call
fmt.Println(p.Distance(q))  // "5", method call

可以看到,上面的两个函数调用都是Distance,但是却没有发生冲突。第一个Distance的调用实际上用的是包级别的函数geometry.Distance,而第二个则是使用刚刚声明的Point,调用的是Point类下声明的Point.Distance方法。
这种p.Distance的表达式叫做选择器,因为他会选择合适的对应p这个对象的Distance方法来执行。选择器也会被用来选择一个struct类型的字段,比如p.X。(类比C/C++的' . '操作)

方法可看作特殊的函数,那么receiver的类型自然可以是基础类型或指针类型。这会关系到调用时对象实例是否被复制。

type N int

func(n N)value() { //func value(n N)
    n++
    fmt.Printf("v: %p, %v\n", &n,n)
}
func(n *N)pointer() { //func pointer(n*N)
    (*n)++
    fmt.Printf("p: %p, %v\n",n, *n)
}

func main() {
    var a N=25

    a.value()
    a.pointer()

    fmt.Printf("a: %p, %v\n", &a,a)
}

output:(类比之间函数部分说的值传递和引用传递)

v:0xc8200741c8,26     //receiver被复制
p:0xc8200741c0,26
a:0xc8200741c0,26

选择方法的receiver类型的原则:

  • 要修改实例状态,用*T。
  • 无须修改状态的小对象或固定值,建议用T。
  • 大对象建议用*T,以减少复制成本。
  • 引用类型、字符串、函数等指针包装对象,直接用T。
  • 若包含Mutex等同步字段,用*T,避免因复制造成锁操作无效。
  • 其他无法确定的情况,都用*T。

接口 interface

在面向对象编程中,可以这么说:“接口定义了对象的行为”。
在某些动态语言里,接口(interface)也被称作协议(protocol)。准备交互的双方,共同遵守事先约定的规则,使得在无须知道对方身份的情况下进行协作。接口要实现的是做什么,而不关心怎么做,谁来做。

在Go中,interface是一种抽象类型,是一组方法签名,是 duck-type programming 的一种体现。不关心属性(数据),只关心行为(方法)。Go接口实现机制很简洁,只要目标类型方法集内包含接口声明的全部方法,就被视为实现了该接口,无须做显示声明。当然,目标类型可实现多个接口。它与oop非常相似。接口指定类型应具有的方法,类型决定如何实现这些方法。

ps:泛型编程和duck typing
简单来说就说,我们编写的代码不是针对特定的类型(比如适用于int, 不适用于string)才有效,而是大部分类型的参数都是可以工作的,这样我们就实现了泛型。

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> A{3,1,2,4,5};
    std::vector<string> B{"golang", "I", "am"};
    std::sort(A.begin(), A.end());  //after this, A={1,2,3,4,5}
    std::sort(B.begin(), B.end());  //after this, B={"I", "am", "golang"}
    return 0;
}

这里的sort函数就是一个泛型编程例子。对于不同存储不同类型的vector,都能进行排序。

When I see a bird that walks like a duck and swins like a duck and quacks like a duck, I call that bird a duck. – James Whitcomb Riley

结合维基百科的定义,duck typing是面向对象编程语言的一种类型定义方法。我们判断一个对象是神马不是通过它的类型定义来判断,而是判断它是否满足某些特定的方法和属性定义。
例如,在不使用鸭子类型的语言中,我们可以编写一个函数,它接受一个类型为鸭的对象,并调用它的走和叫方法。在使用鸭子类型的语言中,这样的一个函数可以接受一个任意类型的对象,并调用它的走和叫方法。如果这些需要被调用的方法不存在,那么将引发一个运行时错误。任何拥有这样的正确的走和叫方法的对象都可被函数接受的这种行为引出了以上表述。

从内部实现来看,接口自身也是一种结构类型,只是编译器会对它做出一些限制:

  • 不能有字段
  • 不能定义自己的方法
  • 只能声明方法,不能实现
  • 可嵌入其它接口类型

接口通常以er作为名称后缀,方法名是声明组成部分,但参数名可不同或省略。

type tester interface{
    test()
    string()string
}
type data struct{}
func(*data)test() {}
func(data)string()string{return"" }
func main() {
    var d data
    //var t tester=d // error:data does not implement tester
    // (test method has pointer receiver)
    var t tester= &d
    t.test()
    println(t.string())
}

编译器根据方法集来判断是否实现了接口,显然在上例中只有*data才复合tester的要求。

空接口

如果接口没有任何方法声明,那么就是一个空接口(interface{}),它的用途类似面向对象里的根类型Object,可被赋值为任何类型的对象。接口变量默认值是nil。如果实现接口的类型支持,可做相等运算。

package main

import (
    "fmt"
)

func describe(i interface{}) {
    fmt.Printf("Type = %T, value = %v\n", i, i)
}

func main() {
     // 任何类型的变量传入都可以

    s := "Hello World"
    i := 55
    strt := struct {
        name string
    }{
        name: "Naveen R",
    }
    describe(s)
    describe(i)
    describe(strt)
}

output:

Type = string, value = Hello World
Type = int, value = 55
Type = struct { name string }, value = {Naveen R}

结合C++的虚函数和多态理解interface

package main

import (
    "fmt"
)

// 薪资计算器接口
type SalaryCalculator interface {
    CalculateSalary() int
}
// 普通挖掘机员工
type Contract struct {
    empId  int
    basicpay int
}
// 有蓝翔技校证的员工
type Permanent struct {
    empId  int
    basicpay int
    jj int // 奖金
}

func (p Permanent) CalculateSalary() int {
    total:= p.basicpay + p.jj
    fmt.Println(total)
    return total
}

func (c Contract) CalculateSalary() int {
    fmt.Println(c.basicpay)
    return c.basicpay
}
// 总开支
func totalExpense(s []SalaryCalculator) {
    expense := 0
    for _, v := range s {
        expense = expense + v.CalculateSalary()
    }
    fmt.Printf("总开支 $%d", expense)
}

func main() {
    pemp1 := Permanent{1,3000,10000}
    pemp2 := Permanent{2, 3000, 20000}
    cemp1 := Contract{3, 3000}
    employees := []SalaryCalculator{pemp1, pemp2, cemp1}
    totalExpense(employees)
}

output:

13000
23000
3000
总开支 $39000

接口嵌入

嵌入其他接口类型,相当于将其声明的方法集导入。这就要求不能有同名方法,因为不支持重载。还有,不能嵌入自身或循环嵌入,那会导致递归错误。

type stringer interface{
    string() string
}
type tester interface{
    stringer // 嵌入其他接口
    test()
}
type data struct{}
func (*data) test() {}
func (data) string() string{
    return""
}
func main() {
    var d data
    var t tester = &d
    t.test()
    println(t.string())
}

接口的执行机制

接口使用一个名为itab的结构存储运行期所需的相关类型信息。

type iface struct{
    tab *itab // 类型信息
    data unsafe.Pointer // 实际对象指针
}
type itab struct{
    inter *interfacetype // 接口类型
    _type *_type // 实际对象类型
    fun [1]uintptr // 实际对象方法地址
}

类型断言

类型断言是一个使用在接口值上的操作。语法上它看起来像x.(T)被称为断言类型,这里x表示一个接口的类型和T表示一个类型。一个类型断言检查它操作对象的动态类型是否和断言的类型匹配。

package main

import(
"fmt"
)

func assert(i interface{}){
    s:= i.(int)
    fmt.Println(s)
}

func main(){
  var s interface{} = 55
  assert(s)
}

如果赋值给s的是一个string类型,断言是 就会引发一个panic

对这种方法稍加改动,我们可以做到类型判断。
在类型断言的语法i.(type)中,类型type应该由类型转换的关键字type替换。

package main

import (  
    "fmt"
)

func findType(i interface{}) {  
    switch i.(type) {
    case string:
        fmt.Printf("String: %s\n", i.(string))
    case int:
        fmt.Printf("Int: %d\n", i.(int))
    default:
        fmt.Printf("Unknown type\n")
    }
}
func main() {  
    findType("Naveen")
    findType(77)
    findType(89.98)
}

output

String: Naveen
Int: 77
Unknown type

推荐阅读更多精彩内容