golang 快速入門 [8.1]-變數型別、宣告賦值、作用域宣告週期與變數記憶體分配

weishixianglian發表於2020-03-27

golang 快速入門 [8.1]-變數型別、宣告賦值、作用域宣告週期與變數記憶體分配

前文

題記

  • 在上一篇文章中,我們介紹了吸心大法—go module 的技巧來引用第三方程式碼,但是看過武俠小說的同學都知道,只有內力沒有招式那也是花架子。正所謂"巧婦難為無米之炊",我們將在後面幾章鞏固基本功,介紹 go 語言的語法、基本概念和性質。 ## 前言 我們將在本文中學習到:
  • 變數的內涵
  • 變數的資料型別
  • 變數的多種宣告與賦值
  • 變數的命名
  • 變數的作用域與示例
  • 變數的記憶體分配方式

變數是什麼

  • 在計算機程式設計中,變數 (Variable) 是與關聯的符號名配對的儲存地址 (記憶體地址標識)
  • 變數用於儲存要在計算機程式中引用和操作的資訊。變數還提供了一種使用描述性名稱標記資料的方法,因此讀者和開發人員都可以更清楚地理解程式。可以將將變數視為儲存資訊的空間。
  • 編譯器必須用資料的實際地址替換變數的符號名。儘管變數的名稱,型別和地址通常保持不變,但儲存在地址中的資料可能會在程式執行期間發生更改。

變數的資料型別

  • go 語言是靜態型別的語言,需要在執行前明確變數的資料型別以及大小。如下圖是靜態語言與動態語言的區別。動態語言可以在執行時擴充套件變數的大小。

  • 資料型別是資料的屬性,它告訴編譯器打算如何使用資料

  • 大多數程式語言都支援基本資料型別,包括整數,浮點數,字元和布林值等。資料型別定義了可以對資料執行的操作,資料的含義以及該型別值的儲存方式

  • Go 語言的數值型別包括幾種不同大小的整數、浮點數和複數。每種數值型別都決定了對應的大小範圍和是否支援正負符號。讓我們先從整型數型別開始介紹

  • Go 語言同時提供了有符號和無符號型別的整數運算,有 int8、int16、int32 和 int64 四種截然不同大小的有符號整型數型別,分別對應 8、16、32、64bit 大小的有符號整型數,與此對應的是 uint8、uint16、uint32 和 uint64 四種無符號整型數型別。

  • 還有兩種一般對應特定 CPU 平臺機器字大小的有符號和無符號整數 int 和 uint。其中 int 是應用最廣泛的數值型別。這兩種型別都有同樣的大小,32 或 64bit,但是我們不能對此做任何的假設;因為不同的編譯器卽使在相同的硬體平臺上可能產生不同的大小。

  • Unicode 字元 rune 型別是和 int32 等價的型別,通常用於表示一個 Unicode 碼點。這兩個名稱可以互換使用。同樣 byte 也是 uint8 型別的等價型別,byte 型別一般用於強調數值是一個原始的資料而不是一個小的整數。

  • 最後,還有一種無符號的整數型別 uintptr,沒有指定具體的 bit 大小但是足以容納指標。uintptr 型別只有在底層程式設計是才需要,特別是 Go 語言和 C 語言函式庫或作業系統介面相互動的地方。在介紹指標時,會詳細介紹它。

變數的宣告與賦值

變數的宣告使用var來標識,變數宣告的通用格式如下:

var name type = expression

  • 函式體外部:變數宣告方式 1
var i int
  • 函式體外部:變數宣告方式 2
// 外部連續宣告
var U, V, W float64
  • 函式體外部:變數宣告方式 3
// 賦值不帶型別,自動推斷
var k = 0
  • 函式體外部:變數宣告方式 4
// 外部連續宣告+賦值
var x, y float32 = -1, -2
  • 函式體外部:變數宣告方式 5

    // 外部var括號內部
    var (
    g       int
    u, v, s = 2.0, 3.0, "bar"
    )
    
  • 函式體內部:變數宣告方式 6

    func main() {
    //函式內部的變數宣告  宣告的變數型別必須使用 否則報錯
    var x string
    
  • 函式體內部:變數宣告方式 7

    // 只限函式內部 自動推斷型別
    y := "jonson"
    

變數的命名

  • 名字的長度沒有邏輯限制,但是 Go 語言的風格是儘量使用短小的名字,對於區域性變數尤其是這樣;你會經常看到 i 之類的短名字,而不是冗長的 theLoopIndex 命名。通常來說,如果一個名字的作用域比較大,生命週期也比較長,那麼用長的名字將會更有意義。
  • 在習慣上,Go 語言程式設計師推薦使用 駝峰式 命名,當名字由幾個單片語成時優先使用大小寫分隔,而不是優先用下劃線分隔。因此,在標準庫有 QuoteRuneToASCII 和 parseRequestLine 這樣的函式命名,但是一般不會用 quote_rune_to_ASCII 和 parse_request_line 這樣的命名。而像 ASCII 和 HTML 這樣的縮略詞則避免使用大小寫混合的寫法,它們可能被稱為 htmlEscape、HTMLEscape 或 escapeHTML,但不會是 escapeHtml。

作用域

  • 在程式設計中,一段程式程式碼中所用到的識別符號並不總是有效/可用的,作用域就是識別符號有效可用的程式碼範圍。
  • 在 go 語言中,作用域可以分為 全域性作用域 > 包級別作用域 > 檔案級別作用域 > 函式作用域 > 內部作用域 universe block > package block > file block > function block > inner block

全域性作用域

  • 全域性作用域主要是 go 語言預宣告的識別符號,所有 go 檔案都可以使用。主要包含了如下的識別符號

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

內建常量: true false iota nil

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

包級別作用域

  • 全域性(任何函式之外)宣告的常量,型別,變數或函式的識別符號是包級別作用域
  • 如下例中的變數 x 以及 fmt 包中的函式println 就是包級別作用域
package main

import "fmt"

var x int=5

func main(){

    fmt.Println("mainx:",x)
}
  • 呼叫例子 1
// f1.go
package main

var x int
//-------------------------------------
// f2.go
package main

func f() {
  fmt.Println(x)
}
  • 呼叫例子 2:呼叫另一個包中的函式和屬性:
//testdemo/destdemo.go
package testdemo

import "fmt"

var Birth uint = 23
func Haha(){
    fmt.Println("lalalal")
}
//-------------------------------------
package main  // main/scope.go

import (
    "testdemo"
    "fmt"
)

func main(){

    testdemo.Haha()
    fmt.Println(testdemo.Birth)
}
  • 注意:如果要讓包中的屬性和變數被外部包呼叫,必須要首字母大寫。

檔案級別作用域

  • import 包的識別符號是檔案級別作用域的,只能夠在本檔案中使用
  • 例如下面的程式碼無效,因為 import 是 file block,不能跨檔案
// f1.go
package main

import "fmt"
//-------------------------------------
// f2.go  無效
package main

func f() {
  fmt.Println("Hello World")
}

函式級別作用域

  • 方法接收者(後面介紹),函式引數和結果變數的識別符號的範圍是函式級別作用域,在函式體外部無效,在內部任何位置可見
  • 例如下面函式中的 a,b,c 就是函式級別作用域
func  add(a int,b int)(c int) {
  fmt.Println("Hello World")
  x := 5
  fmt.Println(x)
}

內部作用域

  • 函式宣告的常量和變數是函式內部作用域,其作用域從宣告開始,到最近的一個花括號結束。
  • 例子 1:注意引數的前後順序
//下面的程式碼無效:
func main() {
  fmt.Println("Hello World")

  fmt.Println(x)
    x := 5
}
  • 例子 2:引數不能跨函式使用
//下面的程式碼無效2:
func main() {
  fmt.Println("Hello World")
  x := 5
  fmt.Println(x)
}
//
func test(){
    fmt.Println(x)
}
  • 例子 3:函式內部變數與外部變數重名,使用就近原則
package main

import "fmt"

var x int=5

func test(){

    var x int = 99;
    x = 100;
    // 下面的程式碼輸出結果為: 100
    fmt.Println("testx",x)
}
  • 例子 4:內部花括號
  • 變數 x 的作用域是從 scope3 到 scope5 為止
func main() {
    fmt.Println("Hello World")  // scope1
    {                           // scope2
        x := 5                  // scope3
        fmt.Println(x)          // scope4
    }                           // scope5
}

變數的記憶體分配

我們在前文go 語言是如何執行的-記憶體概述go 語言是如何執行的-記憶體分配 中,詳細介紹了在虛擬記憶體角度其不同的及其功能

  • 對於全域性變數,其儲存在.data.bss段。 我們可以用下面的例項來驗證
// main.go
package main

var aaa int64 = 8
var ddd int64
func main() {
}
  • 在終端中輸入如下指令列印彙編程式碼
$ go tool compile -S main.go

...
"".aaa SNOPTRDATA size=8
        0x0000 08 00 00 00 00 00 00 00
"".ddd SNOPTRBSS size=8
...

  • 從上面的彙編輸出中可以看出, 變數aaa位於 .data段中, 變數ddd位於.bss
  • 對於函式的內部變數,在 go 語言的程式設計規範中並沒有明確的劃分,變數是分配在棧中還是堆中,簡單來說,Go 語言的逃逸分析 (escape analysis) 會分析各個變數的使用狀況,來決定他要放在 stack 還是 heap 段。
  • 一般的變數會在執行時在棧中建立,隨著函式的呼叫而產生,隨著函式的結束而消亡。如果編譯器無法證明函式返回後未引用該變數,則編譯器必須在堆上分配該變數,以避免懸空指標錯誤。另外,如果區域性變數很大,則將其儲存在堆而不是堆疊上可能更有意義
  • 有一些初始化的情況會被分配到.data段中,例如長度大於4的陣列字面量、字串 等,如下所示 func main() { var vvv = [5]int{1,2,3,4,5} var bbb string = "hello" }
  • 在終端中輸入如下指令列印彙編程式碼 即可驗證其存在於.data段中
$ go tool compile -S main.go
...
go.string."hello" SRODATA dupok size=5
    0x0000 68 65 6c 6c 6f                                   hello
type.[5]int SRODATA dupok size=72
"".ddd SNOPTRBSS size=8
...

總結

參考資料

喜歡本文的朋友歡迎點贊分享~

唯識相鏈啟用微信交流群(Go 與區塊鏈技術)

歡迎加微信:ywj2271840211

更多原創文章乾貨分享,請關注公眾號
  • golang 快速入門 [8.1]-變數型別、宣告賦值、作用域宣告週期與變數記憶體分配
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章