golang的注意點
目錄
額狱窘,本來是有的,但貌似簡書不支持[TOC]或者是html語法
我的github地址:我的Github
里面有一個blog财搁,里面會記錄寫學(xué)習(xí)相關(guān)的內(nèi)容蘸炸。
1. 可以返回局部變量的指針
作為少數(shù)包含指針的語言,它與C還是有所不同尖奔。C中函數(shù)不能夠返回局部變量的指針搭儒,因為函數(shù)結(jié)束時局部變量就會從棧中釋放。而golang可以做到返回局部變量的一點
#include <iostream>
using namespace std;
int* get_some() {
int a = 1;
return &a;
}
int main() {
cout << "a = " << *get_some() << endl;
return 0;
}
*這個明顯在c/c++中是錯誤的寫法提茁,a出棧后什么都沒了淹禾。 會發(fā)生一下錯誤:
$ g++ t.cpp
> t.cpp: In function 'int* get_some()':
> t.cpp:4:6: warning: address of local variable 'a' > returned [-Wreturn-local-addr]
> int a = 1;
^
go語言試驗代碼如下:
package main
import "fmt"
func GetSome() *int {
a := 1;
return &a;
}
func main() {
fmt.Printf("a = %d", *GetSome())
}
基本相同的代碼,但是有以下運行結(jié)果
> $ go run t.go
> a = 1
顯然不是go的編譯器識別不出這個問題茴扁,而是在這個問題上做了優(yōu)化铃岔。參考go FAQ的原文:
How do I know whether a variable is allocated on the heap or the stack?
From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
這里的意思就是讓我們無需擔(dān)心返回的指針是空懸指針。我理解的意思是峭火,普通情況下函數(shù)中局部變量會存儲在棧中毁习,但是如果這個局部變量過大的話編譯器可能會選擇將其存儲在堆中,這樣會更加有意義卖丸。還有一種情況纺且,當(dāng)編譯器無法證明在函數(shù)結(jié)束后變量不被引用那么就會將變量分配到垃圾收集堆上∩越總結(jié)一句:編譯器會進行分析后決定局部變量分配在棧還是堆中
嗯载碌。。衅枫。利用這個特性我們可以使用以下方式來達到并發(fā)做某事的作用
func SomeFun() <-chan int {
out := make(chan int)
go func() {
//做一些不可告人的事情嫁艇。。为鳄。
}()
return out
}
2. Go提供的兩種分配原語——內(nèi)建函數(shù)new和make
Go語言提供了兩種分配的原語裳仆,即內(nèi)建函數(shù)new和make。它們做的事情不同孤钦。
new它不會初始化內(nèi)存歧斟,而是將內(nèi)存置零。也就是說new(T)會為類型T的新項分配一個已置零的內(nèi)存空間,并返回它的地址偏形,也就是*T静袖。即它會返回一個指針,這個指針是指向這個類型T的零值的那份空間俊扭。
make的函數(shù)簽名make(T, args)队橙。它僅用于切片、map和chan類型的創(chuàng)建。make會直接返回一個類型為T的值而非指針,當(dāng)然這個值是已初始化過的捐康。用法已切片為例仇矾,例如:
make([]int, 10, 100)
會分配一個容量為100,長度為10的int類型的切片結(jié)構(gòu)解总。
new([]int)
這個會返回一個指向新分配得贮匕,已置零得切片結(jié)構(gòu),即指向nil切片值的指針花枫。
下面例子闡明了new和make之間的區(qū)別:
var p *[]int = new([]int) //分配切片結(jié)構(gòu)刻盐;*p = nil;基本沒用
var v []int = make([]int, 100) //切片v現(xiàn)在引用了一個具有100個int元素的新數(shù)組
//沒必要這么麻煩
var p *[]int = new([]int)
*p = make([]int, 100, 100)
//習(xí)慣用法
v := make([]int, 100)
記住,make只適用于map劳翰、切片和chan且不返回指針敦锌。若要獲得明確的指針,請使用new分配內(nèi)存
3. 復(fù)合字面
在os標準包中有以下代碼佳簸,這個函數(shù)相當(dāng)于其他語言中的構(gòu)造函數(shù)
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := new(File)
f.fd = fd
f.name = name
f.dirinfo = nil
f.nepipe = 0
return f
}
這里顯得代碼顯得過于冗長乙墙,可以使用復(fù)合字面來簡化代碼
func NewFile(fd int, name string) *File {
if fd < 0 {
return nil
}
f := File{fd, name, nil, 0}
return &f
}
由上面的代碼可以知道復(fù)合字面<code>File{fd, name, nil, 0}</code>返回的是一個量的引用而非指針,所以最后返回時需要取地址符。
4. 基礎(chǔ)類型中數(shù)組溺蕉、切片伶丐、map、chan是值類型還是引用類型
這個是很有必要注意的一件事疯特,用為當(dāng)你讓函數(shù)中傳入一個數(shù)組時,能不能改變外部數(shù)值的值呢肛走?這就要考驗到數(shù)值類型是值類型還是引用類型了漓雅。如果是引用類型的話,相當(dāng)于c語言中傳入指針一樣朽色,可以在函數(shù)內(nèi)部改變傳入?yún)?shù)的外部的值邻吞,但是如果是值類型的話,在傳入函數(shù)過程中只是將一份拷貝傳入葫男,故不可在函數(shù)內(nèi)部修改外部的值抱冷。
go語言中的數(shù)組是值類型的,這與其他語言大不一樣梢褐,拿c/c++為例:
#include <iostream>
using namespace std;
const int NUM = 5;
int a[NUM] = {5,4,3,2,1};
void change_a(int arr[],int n) {
for(int i = 0; i < n; i++){
arr[i]--;
}
}
int main() {
change_a(a,NUM);
for(int i = 0; i < NUM; i++) {
cout << "a[" << i << "] = " << a[i] << endl;
}
}
結(jié)果如下:
Administrator@PC-201809211459 MINGW64 ~/Desktop
$ g++ v.cpp -o v.exe
Administrator@PC-201809211459 MINGW64 ~/Desktop
$ ./v.exe
a[0] = 4
a[1] = 3
a[2] = 2
a[3] = 1
a[4] = 0
很顯然旺遮,函數(shù)內(nèi)部改變了形參數(shù)組導(dǎo)致全局變量a數(shù)組發(fā)生了改變
下面是golang的代碼
package main
import "fmt"
var a [5]int = [5]int{5,4,3,2,1}
func changeA(arr [5]int) {
for i := 0; i < 5; i++ {
arr[i]--
}
}
func main() {
changeA(a)
for i,v := range a {
fmt.Println("a[",i,"] = ",v)
}
}
結(jié)果如下:
Administrator@PC-201809211459 MINGW64 ~/Desktop
$ go run v.go
a[ 0 ] = 5
a[ 1 ] = 4
a[ 2 ] = 3
a[ 3 ] = 2
a[ 4 ] = 1
從此可以看出go語言中的數(shù)組是值類型。事實上我們很少用數(shù)組去傳參數(shù)盈咳,因為在 go中如果用數(shù)組傳參的話需要在函數(shù)的參數(shù)形式列表中寫死數(shù)組的大小耿眉,而這種情況在c/c++中是不需要的。
但是go中傳參可以使用切片鱼响,因為切片是引用類型的鸣剪。同上例子如下:
package main
import "fmt"
func main() {
a := []int{5,4,3,2,1}
func(arr []int) {
for i := 0; i < len(arr); i++ {
arr[i]--
}
}(a)
for i,v := range a {
fmt.Println("a[",i,"] = ",v)
}
}
結(jié)果如下:
Administrator@PC-201809211459 MINGW64 ~/Desktop
$ go run vv.go
a[ 0 ] = 4
a[ 1 ] = 3
a[ 2 ] = 2
a[ 3 ] = 1
a[ 4 ] = 0
由此可見golang的數(shù)組是值類型的,但是切片是引用類型的。
記下 []T{}筐骇、map债鸡、chan作為基礎(chǔ)系統(tǒng)類型里的三個引用類型,而且這三個都是可以使用make這個內(nèi)聯(lián)函數(shù)的铛纬。第七點會歸納這部分內(nèi)聯(lián)函數(shù)
5.初始化函數(shù)init
這個函數(shù)比較神奇啊娘锁,我看官方文檔的時候有些看不懂.官方文檔(鏡像網(wǎng)站上的中文官方文檔)的一段
最后,每個源文件都可以通過定義自己的無參數(shù) init 函數(shù)來設(shè)置一些必要的狀態(tài)饺鹃。 (其實每個文件都可以擁有多個 init 函數(shù)莫秆。)而它的結(jié)束就意味著初始化結(jié)束: 只有該包中的所有變量聲明都通過它們的初始化器求值后 init 才會被調(diào)用, 而那些 init 只有在所有已導(dǎo)入的包都被初始化后才會被求值悔详。
除了那些不能被表示成聲明的初始化外镊屎,init 函數(shù)還常被用在程序真正開始執(zhí)行前,檢驗或校正程序的狀態(tài)茄螃。
我在試驗了之后大概得出結(jié)論缝驳,在import某個包的會執(zhí)行該包下所有文件的init函數(shù),執(zhí)行順序與文件在文件系統(tǒng)的排序有關(guān)归苍。
vv.go,main函數(shù)所在文件
package main
import(
"fmt"
"./some"
_ "./another"
)
func init() {
fmt.Println("hello")
}
func main() {
a := []int{5,4,3,2,1}
some.ChangeA(a)
for i,v := range a {
fmt.Println("a[",i,"] = ",v)
}
var c int = 10
fmt.Println(c)
}
package some下有三個文件
some0.go
package some
import "fmt"
func init() {
fmt.Println("some0")
}
some1.go
package some
import (
"fmt"
)
var a int
func init(){
a = 10
fmt.Println("package some init done!",a)
}
func ChangeA(arr []int) {
for i := 0; i < len(arr); i++ {
arr[i]--
}
}
some2.go
package some
import "fmt"
func init() {
fmt.Println("some2")
}
package another下一個文件
another.go
package another
import "fmt"
func init() {
fmt.Println("package another init done!")
}
以上代碼為了節(jié)省空間用狱,某些為了美觀的空行省略了。
最后運行結(jié)果如下:
some0
package some init done! 10
some2
package another init done!
hello
a[ 0 ] = 4
a[ 1 ] = 3
a[ 2 ] = 2
a[ 3 ] = 1
a[ 4 ] = 0
10
假如將another.go的文件小做修改拼弃,修改如下:
package another
import "fmt"
import _ "../some"
func init() {
fmt.Println("package another init done!")
}
得到的結(jié)果不變夏伊,可見,init函數(shù)只會執(zhí)行一遍吻氧,而不是碰到import它所在的包就執(zhí)行溺忧。
6.關(guān)于指針與值
這個我也是比較糊的,所以在這里進行了部分整理和試驗盯孙。估計以后還有更多關(guān)于這點的問題
先把重要點記下:
- 綁定在類型指針*T上的方法可以改變該類型的值鲁森,但是只綁定在類型T上的方法是無法改變該類型的值的。有以下代碼:
package main
import "fmt"
type Si int
func (s *Si)Plus1(a Si) {
*s += a
}
func (s Si)Plus2(a Si) {
s += a
}
func main() {
var s Si = 10
s.Plus1(1)
fmt.Println("after Plus1: s = ",s)
s.Plus2(1)
fmt.Println("after Plus2: s = ",s)
}
運行結(jié)構(gòu)如下:
after Plus1: s = 11
after Plus2: s = 11
可見Plus2并沒有發(fā)揮其作用振惰。
go語言是一門一眼就能看得懂的語言歌溉,其他語言中把成員函數(shù)神奇的封裝在一個類里,但go不是骑晶,函數(shù)在前面的小括號里寫的參數(shù)就是指定了我這個函數(shù)是歸屬于哪個類型的痛垛,而且顯式的將該類型的值傳入函數(shù)了,也就是函數(shù)名前面的括號其實就可以看作是形參列表
- 類型向接口賦值的時候應(yīng)該取地址
package main
import "fmt"
type Si int
type Plus interface {
Plus1(a Si)
Plus2(a Si)
}
func (s *Si)Plus1(a Si) {
*s += a
}
func (s Si)Plus2(a Si) {
s += a
}
func main() {
var s Si = 10
var ss Plus = &s
ss.Plus1(1)
fmt.Println("after Plus1: s = ",s)
}
以上代碼是成立并且是能正確運行的
但是如果將上面的<code>var ss Plus = &s</code>變成<code>var ss Plus = s</code>就會出現(xiàn)編譯錯誤透罢。該編譯錯誤如下:
# command-line-arguments
cmd\tt.go:26:6: cannot use s (type Si) as type Plus in assignment:
Si does not implement Plus (Plus1 method has pointer receiver)
這個編譯錯誤提示很有意思啊榜晦,前半段提醒我們并沒有實現(xiàn)Plus接口,我們可能會認為Si類型明明實現(xiàn)了Plus接口啊羽圃。這是怎么回事呢乾胶?其實看括號里的話結(jié)合最上面未出錯的程序就會明白抖剿,其實編譯器的意思就是*Si實現(xiàn)了接口Plus但是Si并沒有實現(xiàn)。
為什么會出現(xiàn)這種情況呢识窿,其實是因為接口有一個函數(shù)綁定在指針上<code>func (s *Si)Plus1(a Si)</code>斩郎,而Si類型是沒有實現(xiàn)這個函數(shù)的,故沒有實現(xiàn)Plus接口喻频∷跻耍可是為什么<code>func (s Si)Plus1(a Si)</code>綁定在Si上但是*Si也實現(xiàn)了Plus接口。那是因為go編譯器可以自動根據(jù)<code>func (s Si)Plus1(s Si)</code>這個函數(shù)生成<code>func (s *Si)Plus1(a Si)</code>甥温,故而*Si實現(xiàn)了所有函數(shù)锻煌。
當(dāng)然以上自動生成的過程反過來是無法實現(xiàn)的,因為指針的權(quán)限大的原因姻蚓,<code>func (s *Si)Plus1(a Si)</code>可能會改變s的值宋梧,而<code>func (s Si)Plus1(a Si)</code>無法做到,故而編譯器也不會自動生成。
通過以上分析狰挡,我們以以下例子做試驗:
package main
import "fmt"
type Si int
type Plus interface {
Plus1(a Si)
Plus2(a Si)
}
func (s Si)Plus1(a Si) {
s += a
}
func (s Si)Plus2(a Si) {
s += a
}
func main() {
var s Si = 10
var ss Plus = s
ss.Plus1(1)
fmt.Println("after Plus1: s = ",s)
}
運行結(jié)果:
after Plus1: s = 10
為什么是10在第一點已有介紹了捂龄。編譯通過,第二點分析合理加叁!
- 根據(jù)以上例子發(fā)現(xiàn)了一個奇怪的但又不奇怪的現(xiàn)象倦沧。不論是*T還是T都可以直接調(diào)用函數(shù)。而且在對接口賦值時接口聲明部分無需聲明為指針也不能聲明為指針它匕。接口賦值好后展融,不能取內(nèi)容,雖然有些接口看上去是一個指針超凳。
這里不再舉例
考慮到以上三點愈污,我覺得自己有必要養(yǎng)成的幾個習(xí)慣:
1.接口賦值時應(yīng)該最好使用類型指針對其賦值
2.寫成員函數(shù)時遵循最小權(quán)限原則,注意*T和T的區(qū)別
3.在調(diào)用成員函數(shù)時無論是指針還是類型本身都可以直接調(diào)用
4.接口在調(diào)用成員函數(shù)時就直接調(diào)用
7. 切片轮傍、map和chan有關(guān)的內(nèi)聯(lián)函數(shù)
這一部分也是比較亂的一點,關(guān)聯(lián)這三個基本類型的內(nèi)建函數(shù)大致可以分成三類首装。分別與創(chuàng)建创夜、刪除、操作仙逻。
創(chuàng)建:map驰吓、slice、channel的創(chuàng)建一般都是用make函數(shù)來進行內(nèi)存分配系奉。
刪除:delete主要用于map中刪除實例檬贰。嗯,channel的close也放在這項中吧缺亮。
-
操作:len翁涤、cap可用于不同的類型,len可用于string、slice葵礼、array的長度号阿。cap一般返回slice的分配空間的大小。copy用于復(fù)制slice鸳粉。append用于追加slice.
ps:new用于各種類型的內(nèi)存分配不止以上幾種扔涧。
<h6>具體用法如下:</h6>
-
make
1.1. channel: 這里只拿常用類型int做例子:
ch1 := make(chan int) //不帶緩存的channel
ch2 := make(chan int, 1024) //帶緩存的channel
1.2. slice: 針對slice的函數(shù)簽名make([]type,len)和make([]type,len,cap)
slice1 := make([]int, 10) //slice1中有10個初始值為零值的元素
slice2 := make([]int, 10, 100) //slice2中有10個初始值為零的元素,且初始容量為100
1.3. map:簽名make(map[keyType]valueType)
mp := make(map[string]int)
mp["啊啊啊"] = 3
-
append
2.1. slice: append(slice []Type, elems ...Type) []Type
append函數(shù)主要用于向slice的末尾添加元素的届谈,作為一個特殊的存在可以在字節(jié)切片【】byte("hello")中添加字符串string枯夜。它會返回一個被更新過的slice,如果要使用它就需要一個變量接收這個更新的值艰山。例子如下:
slice1 = append(slice1,2,3,4)
slice2 = append(slice2,slice1...)
slice3 := append([]byte("hello"),"world"...)
-
copy
3.1. slice: copy(dst, src []Type) int
這個函數(shù)需要小心的一點湖雹,slice1和slice2兩個長度分別為5和3.還是用代碼表示吧。程剥。劝枣。
//len(slice1)是5
//len(slice2)是3
//i==3,只會復(fù)制slice1的前三個元素到slice2中
i := copy(slice2,slice1)
//i==3,只會將slice2中的前三個元素復(fù)制到slice1中
i = copy(slice1,slice2)
-
len织鲸、cap
4.1. len用于獲取切片和map長度,channel未取元素個數(shù)舔腾,cap用于獲取切片的容量和channel的緩沖容量。簽名:len(v Type) int搂擦,cap(v Type) int
len(ch1)
len(slice1)
len(mp)
cap(slice1)
cap(ch1)
len不只用于這三個數(shù)據(jù)類型稳诚,還包括string、數(shù)組和指向數(shù)組的指針瀑踢。源代碼注釋如下:
// The len built-in function returns the length of v, according to its type:
// Array: the number of elements in v.
// Pointer to array: the number of elements in *v (even if v is nil).
// Slice, or map: the number of elements in v; if v is nil, len(v) is zero.
// String: the number of bytes in v.
// Channel: the number of elements queued (unread) in the channel buffer;
// if v is nil, len(v) is zero.
cap雖然也可以用在數(shù)組和指向數(shù)據(jù)的指針但是其返回內(nèi)容與len函數(shù)相同扳还。源代注釋如下:
// The cap built-in function returns the capacity of v, according to its type:
// Array: the number of elements in v (same as len(v)).
// Pointer to array: the number of elements in *v (same as len(v)).
// Slice: the maximum length the slice can reach when resliced;
// if v is nil, cap(v) is zero.
// Channel: the channel buffer capacity, in units of elements;
// if v is nil, cap(v) is zero.
-
delete
5.1. delete 只用于map刪除元素。簽名:delete(m map[Type]Type1, key Type)
delete(mp,"啊啊啊")
-
close
6.1. channel可以接受和發(fā)送數(shù)據(jù)橱夭,也可以被關(guān)閉氨距。當(dāng)channel關(guān)閉后向channel發(fā)送數(shù)據(jù)的操作會引起panic。但是當(dāng)channel關(guān)閉后棘劣,我們還能向其中取數(shù)據(jù)俏让,若是之前的數(shù)據(jù)還沒有取完那么還可以將這些數(shù)據(jù)取出。當(dāng)緩存的數(shù)據(jù)全部取完后茬暇,仍然可以對channel取數(shù)據(jù)首昔,此時的數(shù)據(jù)為零值數(shù)據(jù)。簽名:close(c chan<- Type)
close(ch)
待續(xù)糙俗。勒奇。。