07月16日, 2018

goweb-030-golang-程序结构

命名

Go语言中的函数名、变量名、常量名、类型名、语句标号和包名等所有的命名,都遵循一个 简单的命名规则:一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意 数量的字母、数字或下划线。大写字母和小写字母是不同的:abcaBc是两个不 同的名字。

关键字

break      default       func     interface   select
case       defer         go       map         struct
chan       else          goto     package     switch
const      fallthrough   if       range       type
continue   for           import   return      var

还有大约30多个预定义的名字,比如int和true等,主要对应内建的常量、类型和函数。

内建常量: true false iota nil

内建类型: int int8 int16 int32 int64
          uint uint8 uint16 uint32 uint64 uintptr
          float32 float64 complex128 complex64
          bool byte rune string error

内建函数: make len cap new append copy close delete
          complex real imag
          panic recover

这些内部预先定义的名字并不是关键字,可以在定义中重新使用它们。

声明

声明语句定义了程序的各种实体对象以及部分或全部的属性。

  • Go语言主要有四种类型的声明语句:varconsttypefunc,分别对应变量、常量、类型和函数实体对象的声明。
  • 一个Go语言编写的程序对应一个或多个以.go为文件后缀名的源文件中。
  • 每个源文件以包的声明语句开始,说明该源文件是属于哪个包。
  • 包声明语句之后是import语句导入依赖的其它包然后是包一级的类型、变量、常量、函数的声明语句。
  • 包一级的各种类型的声明语句的顺序无关紧要。

    //当前程序包名称
    package main
    
    //导入外部包
    import (
    	"fmt"
    )
    
    //常量
    const TEL = 18284151024
    
    //包级别变量
    var name = "zxysilent"
    
    // 新类型
    type Int int
    
    //结构体
    type S struct{}
    
    // 方法
    func (s S) Test() {}
    
    //接口
    type obj interface{}
    
    //函数
    func main() {
    	// 多个变量的声明
    	var a, b, c int
    	// 多个变量的赋值
    	a, b, c = 1, 2, 3
    	// 多个变量的声明同时赋值
    	var d, e, f int = 4, 5, 6
    	// 多个变量的省略类型的声明赋值(编译器推断类型)
    	var g, h, i = 7, 8, 9
    	fmt.Println(a, b, c, d, e, f, g, h, i)
    	fmt.Println("hello,word")
    }
    

变量

var声明语句可以创建一个特定类型的变量,然后给变量附加一个名字,并且设置变量的初始值。

变量声明的一般语法如下:

var 变量名字 类型 = 表达式

其中类型= 表达式两个部分可以省略其中的一个。

默认值

  • 数值类型变量对应的零值是0。
  • 布尔类型变量对应的零值是false 。
  • 字符串类型对应的零值是空字符串。
  • 接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil。
  • 数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值。

零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量。

var s string
fmt.Println(s) // ""

xxx

  • 一个声明语句中同时声明一组变量,或用一组初始化表达式声明并初始化一组变量。
  • 如果省略每个变量的类型,将可以声明多个类型不同的变量(类型由初始化表达式推导):

    var i, j, k int                 // int, int, int
    var b, f, s = true, 2.3, "four" // bool, float64, string
    

初始化表达式可以是字面量或任意的表达式。在包级别声明的变量会在main入口函数执行完成初始化,局部变量将在声明语句被执行到的时候完成初始化。

  • 一组变量也可以通过调用一个函数,由函数返回的多个返回值初始化:

    var f, err = os.Open("filename") //打开文件
    

简短变量声明

在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。
它以名字 := 表达式形式声明变量,变量的类型根据表达式来自动推导。

package main

import "fmt"

func main() {
	z := "zxysilent"
	fmt.Println(z)
}

简短变量声明被广泛用于大部分的局部变量的声明和初始化。 var形式的声明语句往往是用于需要显式指定变量类型的地方,或者因为变量稍后会被重新赋值而初始值无关紧要的地方。

i := 100                  // an int
var boiling float64 = 100 // a float64
var names []string
var err error
var p Point

简短变量声明语句也可以用来声明和初始化一组变量

i, j := 0, 1

:=是一个变量声明语句,而=是一个变量赋值操作。Ⓜ️做题面试才会考

i, j = j, i // 交换 i 和 j 的值

和普通var形式的变量声明语句一样,简短变量声明语句也可以用函数的返回值来声明和初始化变量,像下面的os.Open函数调用将返回两个值:

f, err := os.Open(name)
if err != nil {
	return err
}
f.Close()

简短变量声明左边的变量可能并不是全部都是刚刚声明的。
如果有一些已经在相同的词法域声明过了,那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了。

in, err := os.Open("inputFile")
// ...
out, err := os.Create("outputFile")

简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量

指针

  • var x int声明语句声明一个x变量那么&x表达式将产生一个指向该整数变量的指针
  • 指针对应的数据类型是*int,指针被称之为“指向int类型的指针”。
  • 如果指针名字为p,那么就说“p指针指向变量x”,或者说“p指针保存了x变量的内存地址”。
  • 同时*p表达式对应p指针指向的变量的值。

    x := 1
    p := &x         
    fmt.Println(*p) 
    *p = 2  
    fmt.Println(x)  
    

返回函数中局部变量的地址也是安全的。

var p = f()

func f() *int {
	v := 1
	return &v
}

指针包含了一个变量的地址,将指针作为参数调用函数,可以在函数中通过该指针来更新变量的值。

func add(p *int) int{
	*p++ 
        return *p
}

v := 1
add(&v) 
fmt.Println(add(&v)) 

每次我们对一个变量取地址,或者复制指针,我们都是为原变量创建了新的别名。

new函数

另一个创建变量的方法是调用内建的new函数。 表达式new(T)将创建一个T类型的匿名变量,初始化为T类型的零值,然后返回变量地址,返回的指针类型为*T

p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

用new创建变量和普通变量声明语句方式创建变量没有什么区别,new函数类似是一种语法糖,而不是一个新的基础概念。

func newInt() *int {
	return new(int)
}

func newInt() *int {
	var dummy int
	return &dummy
}

上面的两个函数有着相同的行为。
由于new只是一个预定义的函数,可以将new名字重新定义为别的类型。

func f(old, new int) int {
  return new - old
}

new被定义为int类型的变量名,在函数内部是无法使用内置的new函数。

生命周期

变量的生命周期指的是在程序运行期间变量有效存在的时间段。 - 包一级声明的变量的生命周期和整个程序的运行周期是一致的。 - 局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。 - 函数的参数变量和返回值变量都是局部变量。

package main

import "fmt"

var v = "1.0"

func main() {
	z := "zxysilent"
	fmt.Println(z)
	for i := 0; i < 2; i++ {
		fmt.Println(i)
	}
	fmt.Println(add(10, 20))
}

func add(a, b int) int {
	return a + b
}

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,不是由用var还是new声明变量的方式决定的。

var global *int

func f() {
	var x int
	x = 1
	global = &x
}

func g() {
	y := new(int)
	*y = 1
}

赋值

使用赋值语句可以更新一个变量的值

被赋值的变量 = 新值的表达式
x = 1                    // 命名变量的赋值
*p = true                // 通过指针间接赋值
person.name = "bob"      // 结构体字段赋值
count[x] = count[x] * 10 // 数组、slice或map的元素赋值
count[x] *= 100

数值变量也可以支持++递增和--递减语句
❗️只能在后面

v := 1
v++    // 等价方式 v = v + 1;v 变成 2
v--    // 等价方式 v = v - 1;v 变成 1

元组赋值 略

  • 元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。
  • 在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。

    x, y = y, x
    a[i], a[j] = a[j], a[i]
    //调用一个有多个返回值的函数
    f, err = os.Open("foo.txt") 
    

情况列举

v, ok = m[key]        // map
v, ok = x.(T)         // 断言
v, ok = <-ch          // channel 接收
v = m[key]            // map查找,失败时返回零值
v = x.(T)             // type断言,失败时panic异常
v = <-ch              // 管道接收,失败时返回零值(阻塞不算是失败)
_, ok = m[key]        // map返回2个值
_, ok = mm[""], false // map返回1个值
_ = mm[""]            // map返回1个值

可以用下划线空白标识符_来丢弃不需要的值。

_, err = io.Copy(dst, src) // 丢弃字节数
_, ok = x.(T)              // 只检测类型,忽略具体值

类型

变量或表达式的类型定义了对应存储值的属性特征,它们在内部是如何表达的,是否支持一些操作符,以及它们自己关联的方法集等。在任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念✏️。

type 类型名字 底层类型

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

package main

import (
	"fmt"
)

// MyInt type 类型名字 底层类型
type MyInt int

func (i MyInt) String() string {
	return fmt.Sprintf("myint:%d", i)
}

func main() {
	var i MyInt //= 100
	i = 100
	fmt.Println(i)
}

许多类型都会定义一个String方法,因为当使用fmt包的打印方法时,将会优先使用该类型对应的String方法返回的结果打印。

包和文件

一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中,通常一个包所在目录路径的后缀是包的导入路径。
目录结构
alt

⭐️ max

package util

//Max 大的一个
func Max(a, b int) int {
	if a > b {
		return a
	}
	return b
}

⭐️ min

package util

//Min 小的一个
func Min(a, b int) int {
	if a > b {
		return b
	}
	return a
}

⭐️ main

package main

import (
	"fmt"
	"tmp/util"
)

func main() {
	a, b := 10, 20
	fmt.Println(util.Min(a, b))
	fmt.Println(util.Max(a, b))
}

导入包

  • Go语言程序中,每个包都有一个全局唯一的导入路径。
  • 一个导入路径代表一个目录中的一个或多个Go源文件。

除了包的导入路径,每个包还有一个包名,包名一般是短小的名字(⚡️ 并不要求包名是唯一的),包名在包的声明处指定。 一个包的名字和包的导入路径的最后一个字段相同。

临时重命名

package main

import (
	"fmt"
	u "tmp/util"
)

func main() {
	a, b := 10, 20
	fmt.Println(u.Min(a, b))
}

省掉包名

package main

import (
	"fmt"
	. "tmp/util"
)

func main() {
	a, b := 10, 20
	fmt.Println(Min(a, b))
}

包的初始化

包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:

var a = b + c // a 第三个初始化, 为 3
var b = f()   // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1     // c 第一个初始化, 为 1

func f() int { return c + 1 }

如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会将.go文件根据文件名排序,然后依次调用编译器编译。 init函数会优先于main函数执行

func init() { /* ... */ }

在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用

✍min

package util

import (
	"fmt"
)

func init() {
	fmt.Println("init min")
}

//Min 小的一个
func Min(a, b int) int {
	if a > b {
		return b
	}
	return a
}

结果
alt

init

alt

作用域 略

  • 预声明标识符的作用域为全域块。
  • 在顶级(即在任何函数之外)声明的表示常量、类型、变量或函数(而非方法)的标识符其作用域为该包块。
  • 已导入包的包名作用域为包含该导入声明的文件块。
  • 表示方法接收器、函数形参或返回值变量的标识符,其作用域为该函数体。
  • 在函数中声明为常量或变量的标识符,其作用域始于该函数中具体常量实现或变量实现ShortVarDecl表示短变量声明)的结尾,止于最内部包含块的结尾。
  • 在函数中声明为类型的标识符,其作用域始于该函数中具体类型实现的标识符,止于最内部包含块的结尾。

    func main() {
    	x := "hello!"
    	for i := 0; i < len(x); i++ {
    		x := x[i]
    		if x != '!' {
    			x := x + 'A' - 'a'
    			fmt.Printf("%c", x) // "HELLO" 
    		}
    	}
    }
    

本文链接:/posts/goweb-030/

-- EOF --