[Go语言入门] 02 Go语言程序结构

文章目录

    • 02 Go语言程序结构
      • 2.1 Go语言的一些基本概念
      • 2.2 go源文件的代码结构
      • 2.3 Go项目的基本文件结构
      • 2.4 实战:创建一个模块
      • 2.5 实战:从另一个模块调用刚创建的模块
      • 2.6 Go程序的初始化过程

02 Go语言程序结构

2.1 Go语言的一些基本概念

变量

Go语言变量源于数学中变量的概念。变量表示一块内存区域,用来存放数据。变量有一个名字,称作变量名。通过变量名可以访问变量,读写变量的内存数据。

举例:

var a = 1            // 变量a,存放数字1
var b = 2            // 变量b,存放数字2
var c = "hello"      // 变量c,存放字符串"hello"

数据类型

Go语言的变量中存放的数据是有类型的。数据类型是对变量中存放的数据的某种解释方式,即程序把内存中的数据看做是什么样的值。

Go语言的数据类型可分为:基本数据类型和派生数据类型。

常见的基本数据类型有:布尔型(bool)、数值型(int,int32,int64,uint,uint32,uint64,…)、字符串型(string)。

函数

函数代表一系列先后执行的操作。函数有一个名字,称作函数名。通过函数名可以调用函数,执行其所代表的操作。

举例:

package main

import "fmt"

// 函数sumOfSquares返回两个数的平方和
func sumOfSquares(a int, b int) int {
  var c = a*a + b*b
  return c
}

func main() {
  var sum int
  sum = sumOfSquares(3, 5)          // 调用函数sumOfSquares
  fmt.Printf("sum = %d\n", sum)
  sum = sumOfSquares(5, 6)
  fmt.Printf("sum = %d\n", sum)     // 调用函数sumOfSquares
}

注释

/*
 这是多行注释。
 这是它的第二行。
 这是它的第三行。
 */

// 这是单行注释

包由一个或多个go源文件聚合而成。这些go源文件存放在同一目录下,这个目录称作包目录。

包是对这些go源文件中声明的数据类型、变量、函数等的封装。

可以设置包中的一些名称是导出的,以便外部包使用;非导出的名称,仅可在包内部使用。

包给包中的代码提供了独立的命名空间,不同包中的相同名称不会冲突。

模块

模块由一个或多个包聚合而成。这些包的包目录放在同一顶层目录下,这个顶层目录称作模块目录。

Go语言是以模块为单位定义编译时需要的上下文的,包括:使用的Go版本、依赖的其他模块以及版本。


2.2 go源文件的代码结构

// 当前程序的包声明,说明当前程序所属的包
package main

// 导入其他包
import "fmt"

// 常量定义
const PI = 3.14

// 全局变量的声明和赋值
var name = "gopher"

// 一般类型声明
type newType int

// 结构的声明
type gopher struct{}

// 接口的声明
type golang interface{}

// 函数的声明
func circleArea(r float32) float32 {
  return PI*r*r;
}

// 由main函数作为程序入口点启动
func main() {
  var text = "Hello, World!"
  fmt.Println(text)
  var area = circleArea(5)
  fmt.Println(area)
}

2.3 Go项目的基本文件结构

Go项目的文件组织结构一般如下:

+ 项目目录
    + 模块1目录 (可以含有go文件,此时也作为一个包目录)
        + uvw.go
        + xyz.go
        + 包a目录
            + a.go
        + 包b目录
            + b.go
            + b2.go
        + 包c目录
            + c.go
            + 子包d目录
                + d.go
    + 模块2目录
        + ...

Go项目的组织结构:

  • 项目目录下包含一个或多个模块目录;
  • 模块目录下包含一个或多个go源文件/包目录;
  • 包目录下面包含一个多个go源文件/子包目录;
  • 子包目录还可以再包含go源文件和子包目录。

2.4 实战:创建一个模块

  1. 创建一个项目目录prj1.my.com,在其下再创建一个模块目录greetings:

    $ mkdir prj1.my.com
    $ cd prj1.my.com
    $ mkdir greetings
    $ cd greetings
    
  2. 使用go mod init初始化greetings模块:

    $ go mod init prj1.my.com/greetings
    go: creating new go.mod: module prj1.my.com/greetings
    

    上面的go mod init命令指定了prj1.my.com/greetings作为模块路径。当模块发布以后,别人的项目将通过这个URL来下载模块。

    go mod init命令会在当前目录生成一个go.mod文件。如果一个目录中有go.mod文件,Go就把该目录当作是一个模块目录。新生成的go.mod文件中仅定义了模块路径和使用的Go版本,如下:

    module prj1.my.com/greetings
    
    go 1.13
    

    以后,当你需要在你的模块中使用其他模块时,这个文件中还会增加对其他模块的依赖,并且可以指定具体依赖的版本。

  3. 在greetings目录下新建代码文件greetings.go:

    package greetings
    
    import "fmt"
    
    // Hello returns a greeting for the named person.
    func Hello(name string) string {
        // Return a greeting that embeds the name in a message.
        message := fmt.Sprintf("Hi, %v. Welcome!", name)
        return message
    }
    

2.5 实战:从另一个模块调用刚创建的模块

  1. 回到prj1.my.com目录,在其下创建另一个模块目录hello:

    cd ..
    mkdir hello
    cd hello
    
  2. 在hello目录下新建代码文件hello.go:

    package main
    
    import (
        "fmt"
    
        "prj1.my.com/greetings"
    )
    
    func main() {
        // Get a greeting message and print it.
        message := greetings.Hello("Gladys")
        fmt.Println(message)
    }
    
  3. 把hello目录初始化为模块:

    $ go mod init hello
    go: creating new go.mod: module hello
    
  4. 设置hello模块使用本地的greetings模块:

    如果greetings模块是作为一个产品发布的话,通常会有一个URL,该URL可以是互联网的地址,也可以是公司内部服务器的地址,Go会自动从该URL下载模块代码。但是现在,我们的greetings项目并不是作为产品发布的,而是在本地文件系统中。因此,需要在hello模块中将URL替换为本地路径。

    1. 在hello项目的go.mod中添加replace指令,

      module hello
      
      go 1.13
      
      replace prj1.my.com/greetings => ../greetings
      

      replace指令告诉Go,查找模块时候使用后面本地路径替换前面的模块路径。

    2. 在hello目录执行go build。go build编译代码的时候检测到代码中依赖了"prj1.my.com/greetings"包,然后定位到本地的greetings模块,并把它作为依赖项添加到go.mod中。

      执行完go build之后,go.mod变为这样:

      module hello
      
      go 1.13
      
      replace prj1.my.com/greetings => ../greetings
      
      require prj1.my.com/greetings v0.0.0-00010101000000-000000000000
      

      将来如果greetings作为产品发布了,想让hello模块切换为使用已发布的greetings,只需要移除hello模块的go.mod文件中的replace指令,并且修改require指令使其指定一个具体的模块版本号。

      module hello
      
      go 1.13
      
      require prj1.my.com/greetings v1.1.0
      
  5. 在hello目录执行刚刚生成的二进制文件

$ ./hello 
Hi, Gladys. Welcome!

2.6 Go程序的初始化过程

思考

下面的代码执行后输出的内容:

package main

import "fmt"

var g = globalVar()

func main() {
	fmt.Println("main")
	fmt.Printf("g = %d\n", g)
}

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

func globalVar() int {
	fmt.Println("globalVar")
	return 3
}

实测结果:

globalVar
init
main
g = 3

从以上结果可以得出:

  1. 全局变量初始化最先被执行;
  2. 全局变量初始化之后,init()函数执行。但奇怪的是,代码中并没有调用它;
  3. 在init()函数执行之后,main()函数执行。

init()函数:

在Go语言中,init()函数是一个特殊的函数。每个go源文件中都可以定义一个init()函数,当然,go源文件中也可以不定义init()函数。

init()函数不需要显式调用,它会在包初始化的时候被自动调用。

如果某个包中的多个go源文件都定义了init()函数,并不会导致命名冲突,但这些init()函数的执行先后顺序是不确定的。

单个包的初始化过程:

(当在代码中import一个包时,会执行这个包的初始化。无论一个包在其他地方被import了多少次,都只会初始化一次。)

  • 初始化一个包时,首先对各个go源文件中import导入的其他包执行初始化(每个被导入包的初始化也遵循本过程)。单个源文件中的import语句按出现顺序执行,多个源文件中的import语句执行顺序不确定。
  • 当本包中的所有import语句都执行完毕后,对各个源文件中的全局变量执行初始化,单个源文件中的全局变量按出现顺序执行,多个源文件中的全局变量执行顺序不确定。
  • 当本包中所有全局变量初始化完毕后,执行各个源文件中的init()函数。多个源文件中的init()函数执行顺序不确定。

如果一个包导入了其他包,被导入的包又导入了其他包,层层导入。那么按照依赖优先原则,最里层被导入的包要首先进行初始化,然后返回外层包,层层向外返回。每单个包的初始化都遵循上面的过程。

main包的初始化:

(main包的初始化也遵照上面的过程)

  • 从执行main包中的所有import语句开始。import语句会触发被import包的初始化。
  • 当main包中的所有import语句执行完毕,main包中的全局变量开始初始化。
  • 当main包中的所有全局变量初始化完毕,main包中的所有init()函数得到执行。
  • 当main包中的所有init()函数执行完毕,main()函数得到执行。

小练习,测试Go程序的初始化过程:

  1. 创建一个目录seeinit,进入该目录,执行go mod init初始化模块:

    $ mkdir seeinit
    $ cd seeinit
    $ go mod init myexample.com/seeinit
    go: creating new go.mod: module myexample.com/seeinit
    
  2. 在seeinit目录下创建子目录apple,该目录用来存放包apple的源码,在apple目录下创建源码文件apple.go、apple2.go。

    apple.go:

    package apple
    
    import "fmt"
    
    var Count = valueOfCount()
    
    func valueOfCount() int {
    	fmt.Println("apple.valueOfCount()")
    	return 1
    }
    
    func init() {
    	fmt.Println("apple.init() 111")
    }
    

    apple2.go:

    package apple
    
    import "fmt"
    
    var count2 = valueOfCount2()
    
    func valueOfCount2() int {
    	fmt.Println("apple.valueOfCount2()")
    	return 2
    }
    
    func init() {
    	fmt.Println("apple.init() 222")
    }
    
  3. 在seeinit目录下创建子目录orange,该子目录用来存放包orange的源码。在orange目录下创建源码文件orange.go、orange2.go。

    orange.go:

    package orange
    
    import "fmt"
    
    var Count = valueOfCount()
    
    func valueOfCount() int {
    	fmt.Println("orange.valueOfCount()")
    	return 2
    }
    
    func init() {
    	fmt.Println("orange.init() 111")
    }
    

    orange2.go:

    package orange
    
    import "fmt"
    
    var count2 = valueOfCount2()
    
    func valueOfCount2() int {
    	fmt.Println("orange.valueOfCount2()")
    	return 3
    }
    
    func init() {
    	fmt.Println("orange.init() 222")
    }
    
  4. 在seeinit目录下创建子目录banana,该子目录下存放包banana的代码,在banana目录下创建源码文件banana.go:

    package banana
    
    import "fmt"
    
    var Count = valueOfCount()
    
    func valueOfCount() int {
    	fmt.Println("banana.valueOfCount")
    	return 5
    }
    
    func init() {
    	fmt.Println("banana init")
    }
    
    
  5. 在seeinit目录下创建main包文件seeit.go、seeinit2.go:

    seeinit.go:

    package main
    
    import (
        "fmt"
        "myexample.com/seeinit/apple"
        "myexample.com/seeinit/orange"
    )
    
    var base = valueOfBase()
    
    func valueOfBase() int {
    	fmt.Println("main.valueOfBase()")
    	return 5
    }
    
    var sum int
    
    func init() {
    	fmt.Println("main.init() 111")
    	sum = base + apple.Count + orange.Count
    }
    
    func main() {
      fmt.Println("main()")
    	fmt.Printf("sum = %d\n", sum)
    }
    

    seeinit2.go:

    package main
    
    import (
        "fmt"
        _ "myexample.com/seeinit/banana"
    )
    
    var base2 = valueOfBase2()
    
    func valueOfBase2() int {
    	fmt.Println("main.valueOfBase2()")
    	return 0
    }
    
    func init() {
    	fmt.Println("main.init() 222")
    }
    
    
  6. 以上程序编写完毕后,在seeinit目录执行go run seeit.go,将看到如下结果:

    从结果中可以印证Go程序的初始化顺序。

    $ go run seeit.go
    apple.valueOfCount()
    apple.valueOfCount2()
    apple.init() 111
    apple.init() 222
    orange.valueOfCount()
    orange.valueOfCount2()
    orange.init() 111
    orange.init() 222
    banana.valueOfCount
    banana init
    main.valueOfBase()
    main.valueOfBase2()
    main.init() 111
    main.init() 222
    main()
    sum = 8
    

Copyright@2022 , [email protected]

你可能感兴趣的:(Go语言入门教程,golang,Go语言入门教程)