日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

Go語言的變量定義詳情_Golang

作者:蘇州程序大白 ? 更新時間: 2022-06-02 編程語言

一、變量

聲明變量

go定義變量的方式和c,c++,java語法不一樣,如下:

var 變量名 類型, 比如 : var a int

var在前,變量名在中間,類型在后面

我們以代碼舉例,如下:

var i int = 0
var i = 0
var i int

以上三個表達式均是合法的,第三個表達式會將i初始化為int類型的零值,0;如果i是bool類型,則為false;i是float64類型,則為0.0;i為string類型,則為"";i為interface類型,則為nil;i為引用類型,則為nil;如果i是struct,則是將struct中所有的字段初始化為對應類型的零值。

這種初始化機制可以保證任何一個變量都是有初始值的,這樣在做邊界條件條件檢查時不需要擔心值未初始化,可以避免一些潛在的錯誤,相信C和C++程序員的體會更加深入。

fmt.Println(s) // ""

這里的s是可以正常打印的,而不是導致某種不可預期的錯誤。

可以在同一條語句中聲明多個變量:

var b, f, s = true, 2.3, "four"http:// bool, float64, string

包內可見的變量在main函數開始執行之前初始化,本地變量在函數執行到對應的聲明語句時初始化。

變量也可以通過函數的返回值來初始化:

var f, err = os.Open(name) // os.Open returns a file and an error

二、短聲明

在函數內部,有一種短聲明的方式,形式是name := expression,這里,變量的類型是由編譯器自動確定的。

anim := gif.GIF{LoopCount: nframes}
freq := rand.Float64() * 3.0
t := 0.0

因為這種形式非常簡潔,因此在函數內部(本地變量)大量使用。如果需要為本地變量顯式的指定類型,或者先聲明一個變量后面再賦值,那么應該使用var:

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

就像var聲明一樣,短聲明也可以并行初始化。

i, j := 0, 1

要謹記的是,:=是一個聲明,=是一個賦值,因此在需要賦值的場所不能使用 :=

var i int
i := 10//panic : no new variables on left side of :=

可以利用并行賦值的特性來進行值交換:

i, j = j, i // swap values of i and j

有一點需要注意的:短聲明左邊的變量未必都是新聲明的!:

//...
out, err := os.Create(path2)?

/因為err已經聲明過,因此這里只新聲明一個變量out。

雖然這里使用:=,但是err是在上個語句聲明的,這里僅僅是賦值/

而且,短聲明的左邊變量必須有一個是新的,若都是之前聲明過的,會報編譯錯誤:

f, err := os.Open(infile)
// ...
f, err := os.Create(outfile) // compile error: no new variables

正確的寫法是這樣的:

f, err := os.Open(infile)
// ...
f, err = os.Create(outfile) // compile ok

指針

值變量的存儲地址存的是一個值。例如 x = 1 就是在x的存儲地址存上1這個值; x[i] = 1代表在數組第i + 1的位置存上1這個值;x.f = 1,代表struct x中的f字段所在的存儲位置存上1這個值。

指針值是一個變量的存儲地址。注意:不是所有的值都有地址,但是變量肯定是有地址的!這個概念一定要搞清楚! 通過指針,我們可以間接的去訪問一個變量,甚至不需要知道變量名。

var x int = 10
p := &x?
/*&x是取x變量的地址,因此p是一個指針,指向x變量.
這里p的類型是*int,意思是指向int的指針*/
fmt.Printf("addr:%p, value:%d\n", p, *p)
//output: addr:0xc820074d98, value:10
*p = 20// 更新x到20

上面的代碼中,我們說p指向x或者p包含了x的地址。p的意思是從p地址中取出對應的變量值,因此p就是x的值:10。因為p是一個變量,因此可以作為左值使用,p = 20,這時代表p地址中的值更新為20,因此這里x會變為20。下面的例子也充分解釋了指針的作用:

x := 1
p := &x ? ? ? ? // p類型:*int,指向x
fmt.Println(*p) // "1"
*p = 2// 等價于x = 2
fmt.Println(x) ?// "2"

聚合類型struct或者array中的元素也是變量,因此是可以通過尋址(&)獲取指針的。

若一個值是變量,那么它就是可尋址的,因此若一個表達式可以作為一個變量使用時,意味著該表達式可以尋址,也可以被使用&操作符。

`指針的零值是nil(記得之前的內容嗎?go的所有類型在沒有初始值時都默認會初始化為該類型的零值)。若p指向一個變量,那么p != nil 就是true,因為p會被賦予變量的地址。指針是可以比較的,兩個指針相等意味著兩個指針都指向同一個變量或者兩個指針都為nil。

var x, y int
fmt.Println(&x == &x, &x == &y, &x == nil) // "true false false" ? ?

在函數中返回一個本地變量的地址是很安全的。例如以下代碼,本地變量v是在f中創建的,從f返回后依然會存在,指針p仍然會去引用v:

var p = f()
fmt.Println(*p) //output:1
func f() *int{
? ? v := 1
return &v
}

每次調用f都會返回不同的指針,因為f會創建新的本地變量并返回指針:

fmt.Println(f() == f()) // "false"
把變量的指針傳遞給函數,即可以在函數內部修改該變量(go的函數默認是值傳遞,所有的值類型都會進行內存拷貝)。

func incr(p *int)int{
? ? ? ? *p++ // increments what p points to; does not change p
return *p
}
v := 1
incr(&v) ? ? ? ? ? ? ?// v現在是2
fmt.Println(incr(&v)) // "3" (and v is 3)

指針在flag包中是很重要的。flag會讀取程序命令行的參數,然后設置程序內部的變量。下面的例子中,我們有兩個命令行參數:-n,不打印換行符;-s sep,使用自定義的字符串分隔符進行打印。

package main
import(
"flag"
"fmt"
"strings"
)
var n = flag.Bool("n", false, "忽略換行符")
var sep = flag.String("s", " ", "分隔符")
func main(){
? ? flag.Parse()
? ? fmt.Print(strings.Join(flag.Args(), *sep))
if !*n {
? ? ? ? ? fmt.Println()
? ? }
}

flag.Bool會創建一個bool類型的flag變量,flag.Bool有三個參數:flag的名字,命令行沒有傳值時默認的flag值(false),flag的描述信息( 當用戶傳入一個非法的參數或者-h、 -help時,會打印該描述信息)。變量sep和n 都是flag變量的指針,因此要通過sep和n來訪問原始的flag值。

當程序運行時,在使用flag值之前首先要調用flag.Parse。非flag參數可以通過args := flag.Args()來訪問,args的類型是[]string(見后續章節)。如果flag.Parse報錯,那么程序就會打印出一個使用說明,然后調用os.Exit(2)來結束。

讓我們來測試一下上面的程序:

$ go build gopl.io/ch2/echo4
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -n a bc def
a bc def$
$ ./echo4 -help
Usage of ./echo4:
? ? ?-n ? ?忽略換行符
? ? ?-s string
? ? ?分隔符 (default" ")

三、new函數

還可以通過內建(built-in)函數new來創建變量。new(T)會初始化一個類型為T的變量,值為類型T對應的零值,然后返回一個指針:*T。

p := new(int) ? // p,類型*int,指向一個沒有命名的int變量
fmt.Println(*p) // "0"
*p = 2
fmt.Println(*p) // "2"

這種聲明方式和普通的var聲明再取地址沒有區別。如果不想絞盡腦汁的去思考一個變量名,那么就可以使用new:

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

每次調用new都會返回一個唯一的地址:

p := new(int)
q := new(int)
fmt.Println(p == q) // "false"

但是有一個例外:比如struct{}或[0]int,這種類型的變量沒有包含什么信息且為零值,可能會有同樣的地址。

new函數相對來說是較少使用的,因為最常用的未具名變量是struct類型,對于這種類型而言,相應的struct語法更靈活也更適合。

因為new是預定義的函數名(參見上一節的保留字),不是語言關鍵字,因此可以用new做函數內的變量名:

func delta(old, new int)int{ returnnew - old }

當然,在delta函數內部,是不能再使用new函數了!

四、變量的生命期

變量的生命期就是程序執行期間變量的存活期。包內可見的變量的生命期是固定的:程序的整個執行期。作為對比,本地變量的生命期是動態的:每次聲明語句執行時,都會創建一個新的變量實例,變量的生命期就是從創建到不可到達狀態(見下文)之間的時間段,生命期結束后變量可能會被回收。

函數的參數和本地變量都是動態生命期,在每次函數調用和執行的時候,這些變量會被創建。例如下面的代碼:

for t := 0.0; t < cycles*2*math.Pi; t += res {
? ? x := math.Sin(t)
? ? y := math.Sin(t*freq + phase)
? ? img.SetColorIndex(size+int(x*size+0.5), size+int(y*size+0.5),
? ? ? ? ? blackIndex)
? ? }

每次for循環執行時,t,x,y都會被重新創建。

那么GC是怎么判斷一個變量應該被回收呢?完整的機制是很復雜的,但是基本的思想是尋找每個變量的過程路徑,如果找不到這樣的路徑,那么變量就是不可到達的,因此就是可以被回收的。

一個變量的生命期只取決于變量是否是可到達的,因此一個本地變量可以在循環之外依然存活,甚至可以在函數return后依然存活。編譯器會選擇在堆上或者棧上去分配變量,但是請記住:編譯器的選擇并不是由var或者new這樣的聲明方式決定的。

var global *int
func f() { ? ? ? ? ? ? ? ? ? ? ?func g(){
? ? var x int ? ? ? ? ? ? ? ? ? ? ? y := new(int)
? ? x = 1 ? ? ? ? ? ? ? ? ? ? ? ? ? *y = 1
? ? global = &x ? ? ? ? ? ? ? ? }
}

上面代碼中,x是在堆上分配的變量,因為在f返回后,x也是可到達的(global指針)。這里x是f的本地變量,因此,這里我們說x從f中逃逸了。相反,當g返回時,變量y就變為不可到達的,然后會被垃圾回收。因為y沒有從g中逃逸,所以編譯器將*y分配在棧上(即使是用new分配的)。在絕大多數情況下,我們都不用擔心變量逃逸的問題,只要在做性能優化時意識到:每一個逃逸的變量都需要進行一次額外的內存分配。

盡管自動GC對于寫現代化的程序來說,是一個巨大的幫助,但是我們也要理解go語言的內存機制。程序不需要顯式的內存分配或者回收,可是為了寫出高效的程序,我們仍然需要清楚的知道變量的生命期。例如,在長期對象(特別是全局變量)中持有指向短期對象的指針,會阻止GC回收這些短期對象,因為在這種情況下,短期對象是可以到達的!!

五、變量的作用域

如果你有c,c++,java的經驗,那么go語言的變量使用域名和這幾門語言是一樣的

一句話: 就近原則,定義在作用域用的變量只能在函數中使用。

如果外面有定義的同名變量,則就近原則。

原文鏈接:https://blog.csdn.net/weixin_46931877/article/details/122406474

欄目分類
最近更新