Go 语言入门指南:基础语法(三) | 青训营

113 阅读12分钟

1. go函数

1.1 函数声明

函数声明包含func关键字,函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。

语法:
func functionname(参数列表) (返回值列表) {函数体}
  • 函数可以没有参数或接受多个参数。
  • 注意类型在变量名之后 。
  • 当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。
  • 函数可以返回任意数量的返回值。
  • 使用关键字 func 定义函数,左大括号依旧不能另起一行。
  • 有返回值的函数,必须有明确的终止语句,否则会引发编译错误。

1.2 参数

函数定义时指出,函数定义时有参数,该变量可称为函数的形参。形参就像定义在函数体内的局部变量。

但当调用函数,传递过来的变量就是函数的实参,函数可以通过两种方式来传递参数。

1.2.1 值传递参数

在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

1.2.2 引用传递参数

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

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

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

}

func main() {
    var a, b int = 1, 2
    /*
        调用 swap() 函数
        &a 指向 a 指针,a 变量的地址
        &b 指向 b 指针,b 变量的地址
    */
    swap(&a, &b)

    fmt.Println(a, b)
}

1.2.3 传递可变参数

不定参数传值 就是函数的参数不是固定的,后面的类型是固定的。Golang 可变参数本质上就是 slice。只能有一个,且必须是最后一个。在参数赋值时可以不用用一个一个的赋值,可以直接传递一个数组或者切片,特别注意的是在参数后加上“…”即可。

func myfunc(args ...int) {    //0个或多个参数
  }

  func add(a int, args…int) int {    //1个或多个参数
  }

  func add(a int, b int, args…int) int {    //2个或多个参数
  }

注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.

任意类型的不定参数: 就是函数的参数和每个参数的类型都不是固定的。 用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。 func myfunc(args ...interface{}) { }

1.2.4 注意

在默认情况下,Go 语言使用的是值传递,即在调用过程中不会影响到实际参数。

  • 注意1:无论是值传递,还是引用传递,传递给函数的都是变量的副本,不过,值传递是值的拷贝。引用传递是地址的拷贝,一般来说,地址拷贝更为高效。而值拷贝取决于拷贝的对象大小,对象越大,则性能越低。

  • 注意2:map、slice、chan、指针、interface默认以引用的方式传递。

1.3 返回值

  • Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 使用_标识符,可用来忽略函数的某个返回值

  • Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。

package main
import "fmt"
func add(x, y int) (z int) {
    z = x + y
    return //会返回z
}

func main() {
    fmt.println(add(1, 2))
}

命名返回参数可被同名局部变量遮蔽,此时需要显式返回。

func add(x, y int) (z int) {
    { // 不能在一个级别,引发 "z redeclared in this block" 错误。
        var z = x + y
        // return   // Error: z is shadowed during return
        return z // 必须显式返回。
    }
}
  • 多返回值可直接作为其他函数调用实参
package main
import "fmt"
func test() (int, int) {
    return 1, 2
}
func add(x, y int) int {
    return x + y
}
func sum(n ...int) int {
    var x int
    for _, i := range n {
        x += i
    }
    return x
}
func main() {
    fmt.println(add(test()))
    femt.println(sum(test()))
}

1.4 匿名函数

在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。可以在函数体后通过括号直接调用,但只能使用一次。

package main

import (
    "fmt"
    "math"
)

func main() {
    a := func(a float64) float64 {
        return math.Sqrt(a)
    }(64)
    fmt.Println(a)
}

Golang匿名函数可赋值给变量,可以通过该变量重复使用

package main

func main() {
    // --- function variable ---
    fn := func() { println("Hello, World!") }
    fn()

    // --- function collection ---
    fns := [](func(x int) int){
        func(x int) int { return x + 1 },
        func(x int) int { return x + 2 },
    }
    println(fns[0](100))
}

1.5 闭包

闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。

“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

package main

import (
    "fmt"
)

func a() func() int {
    i := 0
    b := func() int {
        i++
        fmt.Println(i)
        return i
    }
    return b
}

func main() {
    c := a()
    c()
    c()
    c()

    a() //不会输出i
}
输出结果:
    1
    2
    3

1.6 defer

  • defer是go中一种延迟调用机制,defer后面的函数只有在当前函数执行完毕后才能执行,将延迟的语句按defer的逆序进行执行,也就是说先被defer的语句最后被执行,最后被defer的语句,最先被执行,通常用于释放资源。
多个defer出现的时候,它会把defer之后的函数压入一个栈中延迟执行,也就是先进后出(LIFO),写在前面的defer会比写在后面的defer调用的晚。下面通过一个示例看一下:
package main
import "fmt"
func func1(){
    fmt.Println("我是 func1")
}
func func2(){
    fmt.Println("我是 func2")
}
func func3(){
    fmt.Println("我是 func3")
}
func main(){
    defer func1()
    defer func2()
    defer func3()
    fmt.Println("main1")
    fmt.Println("main2")
}

执行输出如下:
main1
main2
我是 func3
我是 func2
我是 func1
  • 函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。return并不是一个原子操作,没有defer是先赋值,在返回。有defer时赋值后田勇defer后返回。
package main

import "fmt"

func deferFunc() int {
	fmt.Println("defer func called")
	return 0
}

func returnFunc() int {
	fmt.Println("return func called")
	return 0
}

func returnAndDefer() int {

	defer deferFunc()

	return returnFunc()
}

func main() {
	returnAndDefer()
}

执行结果为:
return func called
defer func called

2. go包

2.1 Go语言包的概念

Go语言使用包来组织源代码的,并实现命名空间的管理,任何一个Go语言程序必须属于一个包,即每个go程序的开头要写上package <pkg_name>

Go语言包一般要满足如下三个条件:

  • 同一个目录下的同级的所有go文件应该属于一个包
  • 包的名称可以跟目录不同名,不过建议同名
  • 一个Go语言程序有且只有一个main函数,他是Go语言程序的入口函数,且必须属于main包,没有或者多于一个进行Go语言程序编译时都会报错;

2.2 包的引用

在组织Go代码结构和引用包时,都会不可避免会涉及到一个叫做GOPATH的概念,那它究竟是什么呢?

GOPATH是GO语言使用的一个环境变量,使用绝对路径提供项目的工作目录,适合处理大量 Go语言源码、多个包组合而成的复杂工程。实际使用中,可以先通过命令go env来查看一下当前的GOPATH值,然后再决定是不是需要重新设置。

  • 包引用路径
  1. 绝对路径
  2. 相对路径,引用路径从GOPATH/src/开始算起,main.go需要调用包app的函数,而app包在GOPATH/src/study01/app.go那需在main.go程序写上import "study01/app"
  • 单行导入
import "package1"
import "package2`
  • 多行导入
import (
    "package01"
    "package02"
)
  • 点操作我们有时候会看到如下的方式导入包
import(
    . "fmt"
) 

这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调 用的fmt.Println("hello world")可以省略的写成Println("hello world")

  • 起别名

别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字。导入时,可以为包定义别名,语法演示:

import (
  p1 "package1"
  p2 "package2"
  )
// 使用时:别名操作,调用包函数时前缀变成了我们的前缀
p1.Method()
  • _操作如果仅仅需要导入包时执行初始化操作,并不需要使用包内的其他函数,常量等资源。则可以在导入包时,匿名导入。

这个操作经常是让很多人费解的一个操作符,请看下面这个import:

import (
   "database/sql"
   _ "github.com/ziutek/mymysql/godrv"
 ) 

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。也就是说,使用下划线作为包的别名,会仅仅执行init()。

  • 包中标识符的引用

如果一个包要引用另一个包的标识符(比如结构体、变量、常量、函数等),那么首先必须要将其导出,具体做法就是在定义这些标识符的时候保证首字母大写(首字母小写的标识符只能限制在包内引用)。另外在被导出的的结构体或者接口中,首字母大写的字段和方法才能被包外访问

2.3 Go包引用初始化流程

  • init()、main() 是 go 语言中的保留函数。我们可以在源码中,定义 init() 函数。此函数会在包被导入时执行,例如如果是在 main 中导入包,包中存在 init(),那么 init() 中的代码会在 main() 函数执行前执行,用于初始化包所需要的特定资料。

  • 两个函数在定义时不能有任何的参数和返回值。该函数只能由 go 程序自动调用,不可以被引用。

  • init 可以应用于任意包中,且可以重复定义多个。main 函数只能用于 main 包中,且只能定义一个。

  • 在 main 包中的 go 文件默认总是会被执行。

  1. 对同一个 go 文件的 init( ) 调用顺序是从上到下的。
  2. 对同一个 package 中的不同文件,将文件名按字符串进行“从小到大”排序,之后顺序调用各文件中的init()函数。
  3. 对于不同的 package,如果不相互依赖的话,按照 main 包中 import 的顺序调用其包中的 init() 函数。
  4. 如果 package 存在依赖,调用顺序为最后被依赖的最先被初始化,例如:导入顺序 main –> A –> B –> C,则初始化顺序为 C –> B –> A –> main,一次执行对应的 init 方法。
  5. main 包总是被最后一个初始化,因为它总是依赖别的包 一个包被其它多个包 import,但只能被初始化一次

3. struct实现面向对象编程

3.1 struct简介

struct也是复合类型,而非引用类型,复合类型和引用类型是有区别的,复合类型是值传递,引用类型是引用传递

可以用于定义属性和方法实现面向对象编程

声明结构体并定义属性,属性首字母大写可悲其他包访问,小写只能被本包访问
type  Person  strcuct  {
   name  string
   gender int //string
   age  int 
}

3.2 struct声明初始化

type  Person  strcuct  {
   name  string
   gender int //string
   age  int 
}
(1var p struct = Person{} //声明一个p变量类型为Person,可以在{}内进行初始化,(1)按照结构体字段顺序进行赋值(2)按找name : 值即索引的方式初始化
(3)通过p.name=值,进行赋值

(2var p *struct = &Person{} //p为Person类型的指针
(*p).name可简写为p.name,编译时加上*
(3)p := &Person{} //p为Person类型的指针4)p := new(Person) //p为Person类型的指针

3.3 struct实现方法

在Go语言中,将函数绑定到具体的类型中,则称该函数是该类型的方法,其定义的方式是在func与函数名称之间加上具体类型变量,这个类型变量称为方法接收器

语法为:func ()

type  Person  strcuct  {
   Name  string
   gender int //string
   age  int 
}

值传递
func (p Person) 函数名(参数列表) (返回值列表) {函数体}

引用传递
func (p *Person) 函数名(参数列表) (返回值列表) {函数体}


func (m *Person) setName(name string) {
    m.Name = name 
} 

func (m Person) GetName() (string){
    return m.Name
}
func main(){
    m := Member{} 
    m.setName("小明") 
    fmt.Println(m.GetName())
}


  


我们可以看出,通过方法接收器可以访问结构体的字段,这类似其他编程语言中的this关键词,但在Go语言中,只是一个变量名而已,我们可以任意命名方法接收器

注意,并不是只有结构体才能绑定方法,任何类型都可以绑定方法,只是我们这里介绍将方法绑定到结构体中。

3.4 匿名字段实现继承

Go语言支持直接将类型作为结构体的字段,而不需要取变量名,这种字段叫匿名字段,如:

type Animal struct {
    Name   string  //名称
    Color  string  //颜色
    Height float32 //身高
    Weight float32 //体重
    Age    int     //年龄
}
//奔跑
func (a Animal)Run() {
    fmt.Println(a.Name + "is running")
}
//吃东西
func (a Animal)Eat() {
    fmt.Println(a.Name + "is eating")
}


type Lion struct {
	Animal //匿名字段
}

func main(){
    var lion = Lion{
        Animal{
            Name:  "小狮子",
            Color: "灰色",
        },
    }
    lion.Run()
    fmt.Println(lion.Name)
}

通过该匿名字段可以访问该匿名字段的属性和方法,可以通过实例名.匿名字段名.属性名/方法名进行区分相同的属性或方法名,若不适用匿名字段名遵循就近原则。