golang快速入門[4]-go語言如何編譯爲機器碼windows
在常量和自動類型推斷的文章中,咱們介紹過整數、浮點數在詞法解析階段的過程。簡單的說,整數是全爲數字的常量,浮點數是帶了小數點
的常量。字符串也同樣,字符串常量聲明有兩種方式:
var a string = `hello world` var b string = "hello world"
詞法解析階段,挨個的讀取Uft-8字符, 當發現了單撇號
或者是雙引號
時,說明其是一個字符串。解析函數以下
func (s *scanner) next() { ... c := s.getr() for c == ' ' || c == '\t' || c == '\n' && !nlsemi || c == '\r' { c = s.getr() } // token start s.line, s.col = s.source.line0, s.source.col0 if isLetter(c) || c >= utf8.RuneSelf && s.isIdentRune(c, true) { s.ident() return } switch c { case '"': s.stdString() case '`': s.rawString() ... }
解析時單撇號
會調用rawString,雙引號
會調用stdString,二者略微有所不一樣
單撇號
比較簡單,始終要尋找下一個配對的單撇號
func (s *scanner) rawString() { s.startLit() for { r := s.getr() if r == '`' { break } if r < 0 { s.errh(s.line, s.col, "string not terminated") break } } // We leave CRs in the string since they are part of the // literal (even though they are not part of the literal // value). s.nlsemi = true s.lit = string(s.stopLit()) s.kind = StringLit s.tok = _Literal }
雙引號
有所不一樣,其調用stdString函數。
func (s *scanner) stdString() { s.startLit() for { r := s.getr() if r == '"' { break } if r == '\\' { s.escape('"') continue } if r == '\n' { s.ungetr() // assume newline is not part of literal s.error("newline in string") break } if r < 0 { s.errh(s.line, s.col, "string not terminated") break } } s.nlsemi = true s.lit = string(s.stopLit()) s.kind = StringLit s.tok = _Literal }
當出現另外一個雙引號
則直接退出,當出現了字符`,表明會對後面的字符進行轉義。</li> <li>
雙引號`不能出現以下的換行符,會報錯。
str := " 微信: 1131052403 "
不管是標準字符串仍是原始字符串最終都會被標記成 StringLit 類型的 Token 並傳遞到編譯的下一個階段
s.lit = string(s.stopLit()) 將解析到的字節轉換爲字符串,例如"hello" 最後會被解析爲""hello""
// go/src/cmd/compile/internal/gc func (p *noder) basicLit(lit *syntax.BasicLit) Val { case syntax.StringLit: if len(s) > 0 && s[0] == '`' { // strip carriage returns from raw string s = strings.Replace(s, "\r", "", -1) } // Ignore errors because package syntax already reported them. u, _ := strconv.Unquote(s) return Val{U: u} default: panic("unhandled BasicLit kind") } }
不管是 import 語句中包的路徑、結構體中的字段標籤仍是表達式中的字符串都會使用strings.Replace
方法將原生字符串中最後的換行符刪除並對字符串 Token 進行 Unquote(strconv.Unquote(s)
),也就是去掉字符串兩邊的引號等無關干擾,還原其原本的面目。例如將""hello"" 轉換爲 "hello"
op操做爲:OADDSTR
常量中的字符串函數會在語法分析階段調用sum函數進行拼接。例如對於"hello"+"world"
,會在noder.sum函數中完成拼接。
/usr/local/go/src/cmd/compile/internal/gc/noder.go func (p *noder) sum(x syntax.Expr) *Node { for i := len(adds) - 1; i >= 0; i-- { add := adds[i] r := p.expr(add.Y) if Isconst(r, CTSTR) && r.Sym == nil { if nstr != nil { // Collapse r into nstr instead of adding to n. chunks = append(chunks, r.Val().U.(string)) continue } nstr = r chunks = append(chunks, nstr.Val().U.(string)) } else { if len(chunks) > 1 { nstr.SetVal(Val{U: strings.Join(chunks, "")}) } nstr = nil chunks = chunks[:0] } n = p.nod(add, OADD, n, r) } if len(chunks) > 1 { nstr.SetVal(Val{U: strings.Join(chunks, "")}) } return n }
可是若是是變量之間的拼接,例如對於以下代碼,其拼接操做是在運行時完成的。
var a = "hello" str := a + "xxs"
在語法分析階段會作一些準備工做。例如在類型檢查階段typecheck1
函數進行賦值和字符串拼接語義。
在walkexpr函數中,還會進行準備工做,決定使用運行時的哪個拼接函數。
go/src/cmd/compile/internal/gc/walk.go func walkexpr(n *Node, init *Nodes) *Node { case OADDSTR: n = addstr(n, init) }
walkexpr函數中調用函數addstr(n, init)
當拼接數量小於等於5個時,會調用運行時concatstring1~concatstring5之中的函數
當字符串的數量大於5個時,調用運行時concatstrings函數,而且字符串經過切片
傳入
func addstr(n *Node, init *Nodes) *Node { // build list of string arguments args := []*Node{buf} for _, n2 := range n.List.Slice() { args = append(args, conv(n2, types.Types[TSTRING])) } var fn string if c <= 5 { // small numbers of strings use direct runtime helpers. // note: orderexpr knows this cutoff too. fn = fmt.Sprintf("concatstring%d", c) } else { // large numbers of strings are passed to the runtime as a slice. fn = "concatstrings" t := types.NewSlice(types.Types[TSTRING]) slice := nod(OCOMPLIT, nil, typenod(t)) if prealloc[n] != nil { prealloc[slice] = prealloc[n] } slice.List.Set(args[1:]) // skip buf arg args = []*Node{buf, slice} slice.Esc = EscNone } cat := syslook(fn) r := nod(OCALL, cat, nil) r.List.Set(args) r = typecheck(r, ctxExpr) r = walkexpr(r, init) r.Type = n.Type return r }
運行時字符串string的表示結構爲
type StringHeader struct { Data uintptr Len int }
運行時具體的拼接代碼以下,其實不管使用 concatstring{2,3,4,5} 中的哪個,最終都會調用 runtime.concatstrings,該函數會先對傳入的切片參數進行遍歷,先過濾空字符串並計算拼接後字符串的長度。
/usr/local/go/src/runtime/string.go func concatstrings(buf *tmpBuf, a []string) string { idx := 0 l := 0 count := 0 for i, x := range a { n := len(x) if n == 0 { continue } if l+n < l { throw("string concatenation too long") } l += n count++ idx = i } if count == 0 { return "" } // If there is just one string and either it is not on the stack // or our result does not escape the calling frame (buf != nil), // then we can return that string directly. if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) { return a[idx] } s, b := rawstringtmp(buf, l) for _, x := range a { copy(b, x) b = b[len(x):] } return s } func concatstring2(buf *tmpBuf, a [2]string) string { return concatstrings(buf, a[:]) } func concatstring3(buf *tmpBuf, a [3]string) string { return concatstrings(buf, a[:]) } func concatstring4(buf *tmpBuf, a [4]string) string { return concatstrings(buf, a[:]) } func concatstring5(buf *tmpBuf, a [5]string) string { return concatstrings(buf, a[:]) }
這裏要注意,若是拼接後的字符串大小 小於32字節時,會有一個臨時的緩存供其使用。若是拼接後的字符串大小 大於
32字節時,會請求分配內存。
拼接的過程就是開闢一個足夠大的內存空間,並將多個字符串存入其中的過程。期間會涉及到內存的Copy
拷貝
func rawstringtmp(buf *tmpBuf, l int) (s string, b []byte) { if buf != nil && l <= len(buf) { b = buf[:l] s = slicebytetostringtmp(b) } else { s, b = rawstring(l) } return }
字節數組與字符串相互轉換的形式以下:
a := "微信:1131052403" b := []byte(a) c := string(b)
須要注意的是,字節數組與字符串的相互轉換並非無損的簡單的一個指針的差異。而是涉及到了拷貝!所以相對而言,其仍然是消耗資源的。
以下爲字節數組轉換爲字符串
func slicebytetostring(buf *tmpBuf, b []byte) (str string) { ... var p unsafe.Pointer if buf != nil && len(b) <= len(buf) { p = unsafe.Pointer(buf) } else { p = mallocgc(uintptr(len(b)), nil, false) } stringStructOf(&str).str = p stringStructOf(&str).len = len(b) memmove(p, (*(*slice)(unsafe.Pointer(&b))).array, uintptr(len(b))) return }
func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { *buf = tmpBuf{} b = buf[:len(s)] } else { b = rawbyteslice(len(s)) } copy(b, s) return b }
本節咱們深刻介紹了字符串,字符常量存儲於靜態存儲區,其內容不能夠被改變。聲明時有單撇號
或者是雙引號
兩種方法
字符常量的拼接發生在編譯時,變量字符串的拼接發生在運行時。若是拼接後的字符串大小 小於32字節時,會有一個臨時的緩存供其使用。若是拼接後的字符串大小 大於
32字節時,會請求分配內存
須要注意的是,字節數組與字符串的相互轉換並非無損的簡單的一個指針的差異。而是涉及到了拷貝!所以相對而言,其仍然是消耗資源的
本文還對編譯時和運行時涉及到的函數進行了具體的說明
see you~