From 14792d3f8d0902f8d92acc5cd3501e58b7e81e9f Mon Sep 17 00:00:00 2001 From: Miclle Zheng Date: Sat, 18 Apr 2026 20:59:44 +0800 Subject: [PATCH 1/5] feat(i18n): add internationalization support with Chinese translations - Add URL path prefix routing (/zh/) for language selection - Add locale files (locales/en.json, locales/zh.json) for UI text and title translations - Add Chinese translation .md files for all 112 tutorial sections - Add language switcher dropdown in breadcrumb area - Add per-language caching for parsed examples and rendered index pages - Add gen-translation.go skeleton generator tool - Keep original .gop source files untouched; translations overlay via .md files --- gen-translation.go | 156 +++++++++ locales/en.json | 11 + locales/zh.json | 59 ++++ locales/zh/101-Hello-world/hello-11.md | 5 + locales/zh/101-Hello-world/hello-12.md | 1 + locales/zh/101-Hello-world/hello-2.md | 3 + locales/zh/101-Hello-world/hello-3.md | 13 + locales/zh/102-Values/values.md | 9 + locales/zh/103-Constants/constants.md | 11 + locales/zh/104-Variables/vars.md | 11 + locales/zh/105-Assignments/assign-1.md | 3 + locales/zh/105-Assignments/assign-2.md | 3 + locales/zh/106-Types/types-2.md | 7 + locales/zh/106-Types/types.md | 19 ++ locales/zh/107-Integers/integers.md | 8 + .../108-Floating-Point-Numbers/numbers.md | 8 + locales/zh/109-Complex-Numbers/complex-1.md | 3 + locales/zh/109-Complex-Numbers/complex-2.md | 2 + locales/zh/109-Complex-Numbers/complex-3.md | 4 + locales/zh/110-Booleans/boolean-1.md | 1 + locales/zh/110-Booleans/boolean-2.md | 1 + locales/zh/111-Strings/strings.md | 21 ++ locales/zh/112-Rational-Numbers/rational-1.md | 17 + locales/zh/113-If/Else/if-else.md | 9 + locales/zh/113-Switch/switch-1.md | 11 + locales/zh/114-For/for-0.md | 2 + locales/zh/114-For/for-1.md | 6 + locales/zh/114-For/for-2.md | 2 + locales/zh/114-For/for-3.md | 2 + locales/zh/114-For/for-4.md | 2 + locales/zh/114-For/for-5.md | 2 + locales/zh/114-For/for-6.md | 4 + locales/zh/114-For/for-7.md | 2 + locales/zh/116-Arrays/arrays1.md | 13 + locales/zh/116-Arrays/arrays2.md | 5 + locales/zh/116-Arrays/arrays3.md | 7 + locales/zh/117-Slices/slices-01.md | 4 + locales/zh/117-Slices/slices-02.md | 6 + locales/zh/117-Slices/slices-03.md | 6 + locales/zh/117-Slices/slices-04.md | 8 + locales/zh/117-Slices/slices-05.md | 13 + locales/zh/117-Slices/slices-06.md | 10 + locales/zh/117-Slices/slices-07.md | 4 + locales/zh/117-Slices/slices-08.md | 12 + locales/zh/117-Slices/slices-09.md | 3 + locales/zh/117-Slices/slices-10.md | 10 + locales/zh/117-Slices/slices-11.md | 4 + locales/zh/117-Slices/slices-12.md | 9 + locales/zh/118-Maps/map-0.md | 17 + locales/zh/118-Maps/map-1.md | 11 + locales/zh/118-Maps/map-2.md | 13 + locales/zh/119-Structs/struct-1.md | 9 + locales/zh/119-Structs/struct-2.md | 4 + locales/zh/119-Structs/struct-3.md | 4 + locales/zh/119-Structs/struct-4.md | 4 + locales/zh/119-Structs/struct-5.md | 6 + locales/zh/119-Structs/struct-6.md | 9 + locales/zh/119-Structs/struct-7.md | 1 + locales/zh/120-Pointers/pointer-1.md | 6 + locales/zh/120-Pointers/pointer-2.md | 6 + locales/zh/120-Pointers/pointer-3.md | 6 + locales/zh/120-Pointers/pointer-4.md | 4 + locales/zh/121-For-Each/for-each-1.md | 1 + locales/zh/121-For-Each/for-each-2.md | 6 + locales/zh/121-For-Each/for-each-3.md | 2 + locales/zh/121-For-Range/range.md | 11 + .../zh/121-List-Comprehension/listcompr.md | 9 + locales/zh/201-Functions/funcs.md | 9 + .../202-Multiple-Return-Values/multi-rets.md | 7 + locales/zh/203-Errors/errors.md | 15 + locales/zh/204-Function Values/func-values.md | 1 + locales/zh/205-Closures/closures.md | 1 + .../lambda-expressions-1.md | 3 + .../lambda-expressions-2.md | 1 + .../lambda-expressions-3.md | 1 + locales/zh/206-Recursion/recursion.md | 6 + .../zh/207-Variadic-Parameters/variadic.md | 1 + locales/zh/208-Defer/defer-1.md | 3 + locales/zh/208-Defer/defer-2.md | 3 + locales/zh/209-Exceptions/exceptions-1.md | 5 + locales/zh/209-Exceptions/exceptions-2.md | 5 + locales/zh/210-Methods/methods-1.md | 1 + locales/zh/210-Methods/methods-2.md | 3 + .../ptr-methods-1.md | 11 + .../struct-emb-1.md | 9 + .../struct-emb-2.md | 12 + .../method-values-1.md | 5 + .../method-values-2.md | 5 + .../method-values-3.md | 1 + locales/zh/214-Encapsulation/encap-1.md | 5 + locales/zh/214-Encapsulation/encap-2.md | 3 + locales/zh/214-Encapsulation/encap-3.md | 3 + locales/zh/214-Encapsulation/encap-4.md | 3 + locales/zh/214-Encapsulation/encap-5.md | 3 + locales/zh/214-Encapsulation/encap-6.md | 3 + locales/zh/215-Interfaces/interfaces-1.md | 5 + locales/zh/215-Interfaces/interfaces-2.md | 1 + .../interface-satisfy-1.md | 7 + .../interface-satisfy-2.md | 1 + .../interface-satisfy-3.md | 9 + .../interface-satisfy-4.md | 1 + .../interface-satisfy-5.md | 1 + .../interface-satisfy-6.md | 3 + .../interface-values-1.md | 7 + .../interface-values-2.md | 5 + .../interface-values-3.md | 3 + locales/zh/218-The-error-Interface/error-1.md | 1 + locales/zh/218-The-error-Interface/error-2.md | 3 + locales/zh/218-The-error-Interface/error-3.md | 1 + locales/zh/218-The-error-Interface/error-4.md | 1 + .../zh/219-Type-Assertions/type-assert-1.md | 3 + .../zh/219-Type-Assertions/type-assert-2.md | 1 + .../zh/219-Type-Assertions/type-assert-3.md | 1 + .../zh/219-Type-Assertions/type-assert-4.md | 3 + locales/zh/220-Type-Switches/type-switch.md | 1 + main.go | 298 +++++++++++++++--- public/site.css | 82 ++++- templates/example.tmpl | 44 ++- templates/index.tmpl | 49 ++- 119 files changed, 1244 insertions(+), 76 deletions(-) create mode 100644 gen-translation.go create mode 100644 locales/en.json create mode 100644 locales/zh.json create mode 100644 locales/zh/101-Hello-world/hello-11.md create mode 100644 locales/zh/101-Hello-world/hello-12.md create mode 100644 locales/zh/101-Hello-world/hello-2.md create mode 100644 locales/zh/101-Hello-world/hello-3.md create mode 100644 locales/zh/102-Values/values.md create mode 100644 locales/zh/103-Constants/constants.md create mode 100644 locales/zh/104-Variables/vars.md create mode 100644 locales/zh/105-Assignments/assign-1.md create mode 100644 locales/zh/105-Assignments/assign-2.md create mode 100644 locales/zh/106-Types/types-2.md create mode 100644 locales/zh/106-Types/types.md create mode 100644 locales/zh/107-Integers/integers.md create mode 100644 locales/zh/108-Floating-Point-Numbers/numbers.md create mode 100644 locales/zh/109-Complex-Numbers/complex-1.md create mode 100644 locales/zh/109-Complex-Numbers/complex-2.md create mode 100644 locales/zh/109-Complex-Numbers/complex-3.md create mode 100644 locales/zh/110-Booleans/boolean-1.md create mode 100644 locales/zh/110-Booleans/boolean-2.md create mode 100644 locales/zh/111-Strings/strings.md create mode 100644 locales/zh/112-Rational-Numbers/rational-1.md create mode 100644 locales/zh/113-If/Else/if-else.md create mode 100644 locales/zh/113-Switch/switch-1.md create mode 100644 locales/zh/114-For/for-0.md create mode 100644 locales/zh/114-For/for-1.md create mode 100644 locales/zh/114-For/for-2.md create mode 100644 locales/zh/114-For/for-3.md create mode 100644 locales/zh/114-For/for-4.md create mode 100644 locales/zh/114-For/for-5.md create mode 100644 locales/zh/114-For/for-6.md create mode 100644 locales/zh/114-For/for-7.md create mode 100644 locales/zh/116-Arrays/arrays1.md create mode 100644 locales/zh/116-Arrays/arrays2.md create mode 100644 locales/zh/116-Arrays/arrays3.md create mode 100644 locales/zh/117-Slices/slices-01.md create mode 100644 locales/zh/117-Slices/slices-02.md create mode 100644 locales/zh/117-Slices/slices-03.md create mode 100644 locales/zh/117-Slices/slices-04.md create mode 100644 locales/zh/117-Slices/slices-05.md create mode 100644 locales/zh/117-Slices/slices-06.md create mode 100644 locales/zh/117-Slices/slices-07.md create mode 100644 locales/zh/117-Slices/slices-08.md create mode 100644 locales/zh/117-Slices/slices-09.md create mode 100644 locales/zh/117-Slices/slices-10.md create mode 100644 locales/zh/117-Slices/slices-11.md create mode 100644 locales/zh/117-Slices/slices-12.md create mode 100644 locales/zh/118-Maps/map-0.md create mode 100644 locales/zh/118-Maps/map-1.md create mode 100644 locales/zh/118-Maps/map-2.md create mode 100644 locales/zh/119-Structs/struct-1.md create mode 100644 locales/zh/119-Structs/struct-2.md create mode 100644 locales/zh/119-Structs/struct-3.md create mode 100644 locales/zh/119-Structs/struct-4.md create mode 100644 locales/zh/119-Structs/struct-5.md create mode 100644 locales/zh/119-Structs/struct-6.md create mode 100644 locales/zh/119-Structs/struct-7.md create mode 100644 locales/zh/120-Pointers/pointer-1.md create mode 100644 locales/zh/120-Pointers/pointer-2.md create mode 100644 locales/zh/120-Pointers/pointer-3.md create mode 100644 locales/zh/120-Pointers/pointer-4.md create mode 100644 locales/zh/121-For-Each/for-each-1.md create mode 100644 locales/zh/121-For-Each/for-each-2.md create mode 100644 locales/zh/121-For-Each/for-each-3.md create mode 100644 locales/zh/121-For-Range/range.md create mode 100644 locales/zh/121-List-Comprehension/listcompr.md create mode 100644 locales/zh/201-Functions/funcs.md create mode 100644 locales/zh/202-Multiple-Return-Values/multi-rets.md create mode 100644 locales/zh/203-Errors/errors.md create mode 100644 locales/zh/204-Function Values/func-values.md create mode 100644 locales/zh/205-Closures/closures.md create mode 100644 locales/zh/205-Lambda-expressions/lambda-expressions-1.md create mode 100644 locales/zh/205-Lambda-expressions/lambda-expressions-2.md create mode 100644 locales/zh/205-Lambda-expressions/lambda-expressions-3.md create mode 100644 locales/zh/206-Recursion/recursion.md create mode 100644 locales/zh/207-Variadic-Parameters/variadic.md create mode 100644 locales/zh/208-Defer/defer-1.md create mode 100644 locales/zh/208-Defer/defer-2.md create mode 100644 locales/zh/209-Exceptions/exceptions-1.md create mode 100644 locales/zh/209-Exceptions/exceptions-2.md create mode 100644 locales/zh/210-Methods/methods-1.md create mode 100644 locales/zh/210-Methods/methods-2.md create mode 100644 locales/zh/211-Methods-with-a-Pointer-Receiver/ptr-methods-1.md create mode 100644 locales/zh/212-Composing-Types-by-Struct-Embedding/struct-emb-1.md create mode 100644 locales/zh/212-Composing-Types-by-Struct-Embedding/struct-emb-2.md create mode 100644 locales/zh/213-Method-Values-and-Expressions/method-values-1.md create mode 100644 locales/zh/213-Method-Values-and-Expressions/method-values-2.md create mode 100644 locales/zh/213-Method-Values-and-Expressions/method-values-3.md create mode 100644 locales/zh/214-Encapsulation/encap-1.md create mode 100644 locales/zh/214-Encapsulation/encap-2.md create mode 100644 locales/zh/214-Encapsulation/encap-3.md create mode 100644 locales/zh/214-Encapsulation/encap-4.md create mode 100644 locales/zh/214-Encapsulation/encap-5.md create mode 100644 locales/zh/214-Encapsulation/encap-6.md create mode 100644 locales/zh/215-Interfaces/interfaces-1.md create mode 100644 locales/zh/215-Interfaces/interfaces-2.md create mode 100644 locales/zh/216-Interface-Satisfaction/interface-satisfy-1.md create mode 100644 locales/zh/216-Interface-Satisfaction/interface-satisfy-2.md create mode 100644 locales/zh/216-Interface-Satisfaction/interface-satisfy-3.md create mode 100644 locales/zh/216-Interface-Satisfaction/interface-satisfy-4.md create mode 100644 locales/zh/216-Interface-Satisfaction/interface-satisfy-5.md create mode 100644 locales/zh/216-Interface-Satisfaction/interface-satisfy-6.md create mode 100644 locales/zh/217-Interface-Values/interface-values-1.md create mode 100644 locales/zh/217-Interface-Values/interface-values-2.md create mode 100644 locales/zh/217-Interface-Values/interface-values-3.md create mode 100644 locales/zh/218-The-error-Interface/error-1.md create mode 100644 locales/zh/218-The-error-Interface/error-2.md create mode 100644 locales/zh/218-The-error-Interface/error-3.md create mode 100644 locales/zh/218-The-error-Interface/error-4.md create mode 100644 locales/zh/219-Type-Assertions/type-assert-1.md create mode 100644 locales/zh/219-Type-Assertions/type-assert-2.md create mode 100644 locales/zh/219-Type-Assertions/type-assert-3.md create mode 100644 locales/zh/219-Type-Assertions/type-assert-4.md create mode 100644 locales/zh/220-Type-Switches/type-switch.md diff --git a/gen-translation.go b/gen-translation.go new file mode 100644 index 0000000..f262760 --- /dev/null +++ b/gen-translation.go @@ -0,0 +1,156 @@ +//go:build ignore + +// gen-translation generates skeleton .md translation files for a given language. +// +// Usage: +// +// go run gen-translation.go [tutorial-dir...] +// +// Examples: +// +// go run gen-translation.go zh # generate for all tutorials +// go run gen-translation.go zh 101-Hello-world # generate for specific tutorial +// go run gen-translation.go ja 101-Hello-world 102-Values +// +// The generated .md files contain the original English doc segments as placeholders, +// separated by "---". Translators replace the English text with the target language. +// If a .md file already exists, it will NOT be overwritten. +package main + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +const chNumLen = 3 + +func main() { + if len(os.Args) < 2 { + fmt.Fprintln(os.Stderr, "Usage: go run gen-translation.go [tutorial-dir...]") + os.Exit(1) + } + lang := os.Args[1] + + var dirs []string + if len(os.Args) > 2 { + dirs = os.Args[2:] + } else { + // Find all tutorial directories + fis, err := os.ReadDir(".") + if err != nil { + fmt.Fprintln(os.Stderr, "Error reading current directory:", err) + os.Exit(1) + } + for _, fi := range fis { + if fi.IsDir() { + name := fi.Name() + if len(name) > (chNumLen+1) && name[chNumLen] == '-' { + if _, e := strconv.Atoi(name[:1]); e == nil { + // Skip chapter headings (e.g. 100-, 200-) + if !strings.HasSuffix(name[:chNumLen], "00") { + dirs = append(dirs, name) + } + } + } + } + } + } + + total := 0 + skipped := 0 + for _, dir := range dirs { + fis, err := os.ReadDir(dir) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: cannot read %s: %v\n", dir, err) + continue + } + for _, fi := range fis { + fname := fi.Name() + ext := filepath.Ext(fname) + if ext != ".gop" && ext != ".xgo" { + continue + } + + mdName := strings.TrimSuffix(fname, ext) + ".md" + mdPath := filepath.Join("locales", lang, dir, mdName) + + // Skip if already exists + if _, err := os.Stat(mdPath); err == nil { + skipped++ + continue + } + + // Parse .gop to extract doc segments + content, err := os.ReadFile(filepath.Join(dir, fname)) + if err != nil { + fmt.Fprintf(os.Stderr, "Warning: cannot read %s: %v\n", fname, err) + continue + } + + docs := extractDocSegments(string(content)) + if len(docs) == 0 { + continue + } + + // Create lang directory + langDir := filepath.Join("locales", lang, dir) + if err := os.MkdirAll(langDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Error creating %s: %v\n", langDir, err) + continue + } + + // Write skeleton .md + skeleton := strings.Join(docs, "\n---\n") + if err := os.WriteFile(mdPath, []byte(skeleton+"\n"), 0644); err != nil { + fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", mdPath, err) + continue + } + + fmt.Printf("Created: %s (%d segments)\n", mdPath, len(docs)) + total++ + } + } + fmt.Printf("\nDone: %d files created, %d skipped (already exist)\n", total, skipped) +} + +// extractDocSegments parses a .gop file and returns the text of each doc segment. +func extractDocSegments(content string) []string { + lines := strings.Split(content, "\n") + var segments []string + var current []string + inDoc := false + + for _, line := range lines { + trimmed := strings.TrimSpace(line) + isDoc := false + var docText string + + if strings.HasPrefix(trimmed, "//") { + isDoc = true + docText = strings.TrimPrefix(trimmed[2:], " ") + } else if strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "#!") { + isDoc = true + docText = "##" + trimmed + } + + if isDoc { + current = append(current, docText) + inDoc = true + } else { + if inDoc && len(current) > 0 { + segments = append(segments, strings.Join(current, "\n")) + current = nil + } + inDoc = false + } + } + // Flush last segment + if len(current) > 0 { + segments = append(segments, strings.Join(current, "\n")) + } + + return segments +} diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..4ae56f4 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,11 @@ +{ + "site_title": "XGo by Tutorials", + "breadcrumb_tutorials": "Tutorials", + "index_heading": "Tutorials", + "index_desc_1": "XGo is an open source programming language aimed to enable everyone to become a builder of the world.", + "index_desc_2": "XGo by Tutorials is a hands-on introduction to XGo using annotated example programs. Check out the first example or browse the full list below.", + "no_content_before": "No content yet, you can help us build it ", + "no_content_link": "here", + "no_content_after": ".", + "next_example": "Next example:" +} diff --git a/locales/zh.json b/locales/zh.json new file mode 100644 index 0000000..6a91726 --- /dev/null +++ b/locales/zh.json @@ -0,0 +1,59 @@ +{ + "site_title": "XGo 教程", + "breadcrumb_tutorials": "教程", + "index_heading": "教程", + "index_desc_1": "XGo 是一门开源编程语言,旨在让每个人都能成为世界的构建者。", + "index_desc_2": "XGo 教程是一个通过带注释的示例程序来学习 XGo 的实践教程。查看第一个示例,或浏览下方完整列表。", + "no_content_before": "暂无内容,你可以在", + "no_content_link": "这里", + "no_content_after": "帮助我们完善。", + "next_example": "下一个示例:", + "titles": { + "Sequential programming": "Sequential Programming 顺序式编程", + "Structured programming": "Structured Programming 结构化编程", + "Hello world": "Hello World", + "Values": "Values 值", + "Constants": "Constants 常量", + "Variables": "Variables 变量", + "Assignments": "Assignments 赋值", + "Types": "Types 类型", + "Integers": "Integers 整数", + "Floating-Point Numbers": "Floating-Point Numbers 浮点数", + "Complex Numbers": "Complex Numbers 复数", + "Booleans": "Booleans 布尔值", + "Strings": "Strings 字符串", + "Rational Numbers": "Rational Numbers 有理数", + "If/Else": "If/Else 条件判断", + "Switch": "Switch 分支选择", + "For": "For 循环", + "Arrays": "Arrays 数组", + "Slices": "Slices 切片", + "Maps": "Maps 映射", + "Structs": "Structs 结构体", + "Pointers": "Pointers 指针", + "For Each": "For Each 遍历", + "For Range": "For Range 范围遍历", + "List Comprehension": "List Comprehension 列表推导", + "Functions": "Functions 函数", + "Multiple Return Values": "Multiple Return Values 多返回值", + "Errors": "Errors 错误处理", + "Function Values": "Function Values 函数值", + "Closures": "Closures 闭包", + "Lambda expressions": "Lambda Expressions Lambda 表达式", + "Recursion": "Recursion 递归", + "Variadic Parameters": "Variadic Parameters 可变参数", + "Defer": "Defer 延迟执行", + "Exceptions": "Exceptions 异常处理", + "Methods": "Methods 方法", + "Methods with a Pointer Receiver": "Methods with a Pointer Receiver 指针接收者方法", + "Composing Types by Struct Embedding": "Composing Types by Struct Embedding 结构体嵌入组合", + "Method Values and Expressions": "Method Values and Expressions 方法值与表达式", + "Encapsulation": "Encapsulation 封装", + "Interfaces": "Interfaces 接口", + "Interface Satisfaction": "Interface Satisfaction 接口实现", + "Interface Values": "Interface Values 接口值", + "The error Interface": "The error Interface error 接口", + "Type Assertions": "Type Assertions 类型断言", + "Type Switches": "Type Switches 类型分支" + } +} diff --git a/locales/zh/101-Hello-world/hello-11.md b/locales/zh/101-Hello-world/hello-11.md new file mode 100644 index 0000000..4f88f77 --- /dev/null +++ b/locales/zh/101-Hello-world/hello-11.md @@ -0,0 +1,5 @@ +XGo 的 Hello World 有三种写法。 +--- +### 第一种:命令式风格 +--- +这是我们推荐的写法,非常容易理解。特别是对中小学生来说,命令是最容易理解的逻辑,比函数调用容易得多。 diff --git a/locales/zh/101-Hello-world/hello-12.md b/locales/zh/101-Hello-world/hello-12.md new file mode 100644 index 0000000..8e19d22 --- /dev/null +++ b/locales/zh/101-Hello-world/hello-12.md @@ -0,0 +1 @@ +为了强调我们对命令式风格的偏好,我们引入了 `echo` 作为 `println` 的别名: diff --git a/locales/zh/101-Hello-world/hello-2.md b/locales/zh/101-Hello-world/hello-2.md new file mode 100644 index 0000000..4337490 --- /dev/null +++ b/locales/zh/101-Hello-world/hello-2.md @@ -0,0 +1,3 @@ +## 第二种:函数调用风格 +--- +这种写法更像 Python。理解这种写法的基础是理解什么是函数调用。这并不太难,特别是如果中学生在数学课上学过函数(如 sin),就比较容易理解。大多数编程语言也支持这种标准的函数调用语法。 diff --git a/locales/zh/101-Hello-world/hello-3.md b/locales/zh/101-Hello-world/hello-3.md new file mode 100644 index 0000000..7c2943f --- /dev/null +++ b/locales/zh/101-Hello-world/hello-3.md @@ -0,0 +1,13 @@ +## 第三种:软件工程风格 +--- +这是从 Go 语言继承而来的标准软件工程写法。对初学者来说不太容易理解,因为需要理解什么是函数(func)和什么是包(package)。当然,它也有其优点——让你开始建立功能分解和团队协作的基本逻辑。 +--- +那么,如何体验 XGo 呢? +--- +最简单的方式,直接去 XGo Playground 体验: +--- +* https://play.xgo.dev +--- +在早期学习基础语法时,完全可以用这种方式。 +--- +如何在本地安装 XGo?我们后面会讨论这个话题。 diff --git a/locales/zh/102-Values/values.md b/locales/zh/102-Values/values.md new file mode 100644 index 0000000..ffd3d5f --- /dev/null +++ b/locales/zh/102-Values/values.md @@ -0,0 +1,9 @@ +XGo 有多种值类型,包括字符串、整数、浮点数、布尔值等。 +--- +以下是一些基本示例。 +--- +字符串,可以使用 `+` 进行拼接。 +--- +整数和浮点数。 +--- +布尔值,以及常见的布尔运算符。 diff --git a/locales/zh/103-Constants/constants.md b/locales/zh/103-Constants/constants.md new file mode 100644 index 0000000..0964111 --- /dev/null +++ b/locales/zh/103-Constants/constants.md @@ -0,0 +1,11 @@ +XGo 支持字符、字符串、布尔值和数值类型的_常量_。 +--- +`const` 用于声明一个常量值。 +--- +`const` 语句可以出现在任何 `var` 语句可以出现的地方。 +--- +常量表达式可以执行任意精度的算术运算。 +--- +数值常量在被显式转换等方式赋予类型之前,是没有类型的。 +--- +数值可以通过在需要特定类型的上下文中使用来获得类型,例如变量赋值或函数调用。例如,这里 `math.sin` 期望一个 `float64` 类型的参数。 diff --git a/locales/zh/104-Variables/vars.md b/locales/zh/104-Variables/vars.md new file mode 100644 index 0000000..a228b5d --- /dev/null +++ b/locales/zh/104-Variables/vars.md @@ -0,0 +1,11 @@ +在 XGo 中,_变量_ 需要显式声明,编译器会利用它们来检查函数调用的类型正确性等。 +--- +`var` 用于声明一个或多个变量。 +--- +你可以一次声明多个变量。 +--- +XGo 会自动推断已初始化变量的类型。 +--- +声明时未进行初始化的变量会被赋予_零值_。例如,`int` 的零值是 `0`。 +--- +`:=` 语法是声明并初始化变量的简写形式,例如在这个例子中等价于 `var f string = "apple"`。 diff --git a/locales/zh/105-Assignments/assign-1.md b/locales/zh/105-Assignments/assign-1.md new file mode 100644 index 0000000..a1338bf --- /dev/null +++ b/locales/zh/105-Assignments/assign-1.md @@ -0,0 +1,3 @@ +### 赋值 +--- +`=` 用于赋值。可以在一行中同时修改多个变量的值。通过这种方式,可以在不使用中间变量的情况下交换它们的值。 diff --git a/locales/zh/105-Assignments/assign-2.md b/locales/zh/105-Assignments/assign-2.md new file mode 100644 index 0000000..8ea1d0c --- /dev/null +++ b/locales/zh/105-Assignments/assign-2.md @@ -0,0 +1,3 @@ +### `=` 与 `:=` +--- +`:=` 用于声明并初始化变量。可以在一行中声明并初始化多个变量。 diff --git a/locales/zh/106-Types/types-2.md b/locales/zh/106-Types/types-2.md new file mode 100644 index 0000000..44f514c --- /dev/null +++ b/locales/zh/106-Types/types-2.md @@ -0,0 +1,7 @@ +### XGo 内置了对有理数的支持: + + +我们将有理数作为 XGo 的基本类型引入。我们使用后缀 `r` 来表示有理数字面量。例如,`1r << 200` 表示一个值等于 2^200 的大整数。 + +默认情况下,`1r` 的类型为 bigint。 +而 `4/5r` 表示有理常量 4/5,其类型为 bigrat。 diff --git a/locales/zh/106-Types/types.md b/locales/zh/106-Types/types.md new file mode 100644 index 0000000..612ee59 --- /dev/null +++ b/locales/zh/106-Types/types.md @@ -0,0 +1,19 @@ +### XGo 的基本类型包括 +
+bool
+int8    int16   int32   int    int64    int128
+uint8   uint16  uint32  uint   uint64   uint128
+uintptr (类似于 C 语言的 size_t)
+byte (uint8 的别名)
+rune (int32 的别名,表示一个 Unicode 码点)
+string
+float32 float64
+complex64 complex128
+bigint bigrat
+unsafe.Pointer (类似于 C 语言的 void*)
+any (Go 中 interface{} 的别名)
+
+ +示例展示了多种类型的变量,同时也演示了变量声明可以像 import 语句一样被"分组"到代码块中。 +--- +int、uint 和 uintptr 类型在 32 位系统上通常是 32 位宽,在 64 位系统上是 64 位宽。当你需要一个整数值时,应该使用 int,除非有特定原因需要使用固定大小或无符号的整数类型。 diff --git a/locales/zh/107-Integers/integers.md b/locales/zh/107-Integers/integers.md new file mode 100644 index 0000000..675ea2d --- /dev/null +++ b/locales/zh/107-Integers/integers.md @@ -0,0 +1,8 @@ +int 类型表示整数,可以是正数或负数。int 类型的大小取决于平台,可以是 32 位或 64 位。还有具有特定大小的整数类型,如 int8、int16、int32、int64 和 int128,但除非你需要特定大小,否则应该使用 int 类型。 + +uint 类型表示正整数。uint 类型的大小取决于平台,可以是 32 位或 64 位。还有具有特定大小的无符号整数类型,如 uint8、uint16、uint32、uint64 和 uint128,但除非你需要特定大小,否则应该使用 uint 类型。 + +对于 int 值 20,也可以用十六进制 (0x14)、八进制 (0o24) 和二进制 (0b0010100) 表示。 +uint 没有 uint 字面量,所有整数字面量都被视为 int 值。 + +XGo 还支持使用 `_` 作为数字分隔符,也支持将 bool 转换为数值类型。如示例所示 diff --git a/locales/zh/108-Floating-Point-Numbers/numbers.md b/locales/zh/108-Floating-Point-Numbers/numbers.md new file mode 100644 index 0000000..83442c1 --- /dev/null +++ b/locales/zh/108-Floating-Point-Numbers/numbers.md @@ -0,0 +1,8 @@ +XGo 中有两种浮点类型:float32 和 float64。 +浮点数字面量的默认类型为 float64。 + +浮点数无法精确表示十进制值。不要用它们来表示货币或任何其他需要精确十进制表示的值! + +虽然你可以使用 == 和 != 来比较浮点数,但不建议这样做。由于浮点数的不精确性,两个浮点值可能在你认为它们应该相等时并不相等。相反,应该定义一个最小允许偏差,然后检查两个浮点数的差值是否小于该值。这个最小值(有时称为 epsilon)取决于你的精度需求; + +浮点数字面量也可以用 10 的幂来声明,将一个值为 0 的浮点变量除以 0 会返回 NaN(非数字)。 diff --git a/locales/zh/109-Complex-Numbers/complex-1.md b/locales/zh/109-Complex-Numbers/complex-1.md new file mode 100644 index 0000000..5980b43 --- /dev/null +++ b/locales/zh/109-Complex-Numbers/complex-1.md @@ -0,0 +1,3 @@ +### 初始化复数 +XGo 中有两种复数类型:complex64 和 complex128。 +初始化复数非常简单。你可以使用构造函数或初始化语法。 diff --git a/locales/zh/109-Complex-Numbers/complex-2.md b/locales/zh/109-Complex-Numbers/complex-2.md new file mode 100644 index 0000000..dea7922 --- /dev/null +++ b/locales/zh/109-Complex-Numbers/complex-2.md @@ -0,0 +1,2 @@ +### 复数的组成部分 +复数有两个部分:实部和虚部。我们使用函数来获取它们。 diff --git a/locales/zh/109-Complex-Numbers/complex-3.md b/locales/zh/109-Complex-Numbers/complex-3.md new file mode 100644 index 0000000..86cd6a6 --- /dev/null +++ b/locales/zh/109-Complex-Numbers/complex-3.md @@ -0,0 +1,4 @@ +### 复数的运算 +复数变量可以执行加法、减法、乘法和除法等任何运算。 + +让我们来看一个对复数执行数学运算的示例。 diff --git a/locales/zh/110-Booleans/boolean-1.md b/locales/zh/110-Booleans/boolean-1.md new file mode 100644 index 0000000..cbc118d --- /dev/null +++ b/locales/zh/110-Booleans/boolean-1.md @@ -0,0 +1 @@ +bool 类型表示布尔变量。bool 类型的变量只能有两个值:true 或 false。bool 的零值为 false。 diff --git a/locales/zh/110-Booleans/boolean-2.md b/locales/zh/110-Booleans/boolean-2.md new file mode 100644 index 0000000..d15de3f --- /dev/null +++ b/locales/zh/110-Booleans/boolean-2.md @@ -0,0 +1 @@ +在 XGo 中,你可以将 bool 转换为数值类型。 diff --git a/locales/zh/111-Strings/strings.md b/locales/zh/111-Strings/strings.md new file mode 100644 index 0000000..0282b43 --- /dev/null +++ b/locales/zh/111-Strings/strings.md @@ -0,0 +1,21 @@ +字符串是一个不可变的字节序列。字符串可以包含任何数据,通常用于保存文本。 + +我们一般使用双引号 "" 来定义字符串,但需要注意其中有些特定字符具有特殊含义,我们称之为转义字符,这些转义字符包括: + +
+  \n:换行
+  \r:回车
+  \t:制表符
+  \u 或 \U:Unicode
+  \:反斜杠
+
+--- +如果我们想知道字符串占用的字节长度,可以使用 XGo 的内置函数来计算: +--- +如果我们要定义一个字符串,语法如下: +--- +我们可以使用 + 将两个字符串拼接在一起,将后面的字符串追加到前面字符串的末尾。 +--- +如果我们想定义多行字符串,XGo 也支持。使用传统的 "" 无法跨行,如果要定义多行字符串,可以使用反引号:` +--- +反引号之间的代码不会被编辑器识别为特殊字符,而只会作为字符串的一部分。 diff --git a/locales/zh/112-Rational-Numbers/rational-1.md b/locales/zh/112-Rational-Numbers/rational-1.md new file mode 100644 index 0000000..9b4ceaa --- /dev/null +++ b/locales/zh/112-Rational-Numbers/rational-1.md @@ -0,0 +1,17 @@ +XGo 拥有多种有理数类型,包括 bigint、bigrat 和 bigfloat。 +以下是一些关于 bigint 类型的基本示例。 +--- +### bigint 类型变量的声明与赋值 +--- +XGo 语言使用关键字 "var" 来声明变量。 +--- +整数有理变量可以在声明时赋值。 +(1r<<65),其值等于 2 的 65 次方。 +--- +注意: +未赋值的有理变量的初始值不是 0,而是 ``。 + +预期结果: +bint1: `` +bint2: `` +bint3: `36893488147419103232` diff --git a/locales/zh/113-If/Else/if-else.md b/locales/zh/113-If/Else/if-else.md new file mode 100644 index 0000000..f408121 --- /dev/null +++ b/locales/zh/113-If/Else/if-else.md @@ -0,0 +1,9 @@ +在 XGo 中使用 `if` 和 `else` 进行分支判断非常简单直接。 +--- +这是一个基本示例。 +--- +你可以使用不带 else 的 `if` 语句。 +--- +条件语句之前可以有一个前置语句;在该语句中声明的变量在所有分支中都可用。 +--- +注意,在 XGo 中条件表达式不需要用圆括号括起来,但花括号是必须的。 diff --git a/locales/zh/113-Switch/switch-1.md b/locales/zh/113-Switch/switch-1.md new file mode 100644 index 0000000..73eb304 --- /dev/null +++ b/locales/zh/113-Switch/switch-1.md @@ -0,0 +1,11 @@ +_Switch 语句_ 用于表达跨多个分支的条件判断。 +--- +这是一个基本的 `switch` 示例。 +--- +你可以在同一个 `case` 语句中使用逗号分隔多个表达式。在这个示例中我们还使用了可选的 `default` 分支。 +--- +不带表达式的 `switch` 是表达 if/else 逻辑的另一种方式。这里我们还展示了 `case` 表达式可以是非常量的。 +--- +XGo 中的 switch 默认在每个 case 末尾自动 break。使用 fallthrough 可以强制执行后续 case 的代码。 +--- +带有 `fallthrough` 的 `switch`: diff --git a/locales/zh/114-For/for-0.md b/locales/zh/114-For/for-0.md new file mode 100644 index 0000000..8b26d1b --- /dev/null +++ b/locales/zh/114-For/for-0.md @@ -0,0 +1,2 @@ +### For 循环 +XGo 只有一个循环关键字:for,但有多种形式。 diff --git a/locales/zh/114-For/for-1.md b/locales/zh/114-For/for-1.md new file mode 100644 index 0000000..d144f31 --- /dev/null +++ b/locales/zh/114-For/for-1.md @@ -0,0 +1,6 @@ +### 1、for/in +这是最常见的形式。你可以将它用于切片、映射、数值范围或自定义迭代器。 + +`for value in arr/map` 形式用于遍历切片或映射的元素。 + +如果需要索引,可以使用替代形式 `for index, value in arr`。 diff --git a/locales/zh/114-For/for-2.md b/locales/zh/114-For/for-2.md new file mode 100644 index 0000000..a17740d --- /dev/null +++ b/locales/zh/114-For/for-2.md @@ -0,0 +1,2 @@ +### 2、Range for +你可以在 for 循环中使用范围表达式 (start:end:step)。 diff --git a/locales/zh/114-For/for-3.md b/locales/zh/114-For/for-3.md new file mode 100644 index 0000000..99ad6f4 --- /dev/null +++ b/locales/zh/114-For/for-3.md @@ -0,0 +1,2 @@ +### 3、for/in/if +所有 for/in 形式的循环都可以带有可选的 if 条件。 diff --git a/locales/zh/114-For/for-4.md b/locales/zh/114-For/for-4.md new file mode 100644 index 0000000..7462599 --- /dev/null +++ b/locales/zh/114-For/for-4.md @@ -0,0 +1,2 @@ +### 4、条件 for +可以省略条件,从而形成无限循环。你可以使用 break 或 return 来结束循环。 diff --git a/locales/zh/114-For/for-5.md b/locales/zh/114-For/for-5.md new file mode 100644 index 0000000..e30ca6b --- /dev/null +++ b/locales/zh/114-For/for-5.md @@ -0,0 +1,2 @@ +### 5、C 风格 for +最后是传统的 C 风格 for 循环。它比 while 形式更安全,因为使用 while 时很容易忘记更新计数器而陷入无限循环。 diff --git a/locales/zh/114-For/for-6.md b/locales/zh/114-For/for-6.md new file mode 100644 index 0000000..c9a389b --- /dev/null +++ b/locales/zh/114-For/for-6.md @@ -0,0 +1,4 @@ +### break 和 continue +如何在不使用键盘或关闭电脑的情况下跳出无限 for 循环?这就是 break 语句的作用。它会立即退出循环,就像其他语言中的 break 语句一样。当然,你可以在任何 for 语句中使用 break,而不仅仅是无限 for 语句。 + +XGo 还包含 continue 关键字,它会跳过 for 循环体的剩余部分,直接进入下一次迭代。从技术上讲,你并不一定需要 continue 语句。 diff --git a/locales/zh/114-For/for-7.md b/locales/zh/114-For/for-7.md new file mode 100644 index 0000000..6825bfa --- /dev/null +++ b/locales/zh/114-For/for-7.md @@ -0,0 +1,2 @@ +### 给 "for" 语句添加标签 +默认情况下,break 和 continue 关键字作用于直接包含它们的 for 循环。如果你有嵌套的 for 循环,想要退出或跳过外层循环的某次迭代怎么办?让我们来看一个示例。我们将修改字符串遍历程序,使其在遇到字母 "l" 时立即停止遍历该字符串。 diff --git a/locales/zh/116-Arrays/arrays1.md b/locales/zh/116-Arrays/arrays1.md new file mode 100644 index 0000000..3765fb8 --- /dev/null +++ b/locales/zh/116-Arrays/arrays1.md @@ -0,0 +1,13 @@ +在 XGo 中,_数组_ 是一个具有特定长度的、由相同类型元素组成的编号序列。 +--- +### 一维数组的声明 +--- +这里我们创建了一个恰好可以容纳 5 个 `int` 的数组 `a`。元素类型和长度都是数组类型的一部分。默认情况下,数组是零值的,对于 `int` 来说就是 `0`。 +--- +我们可以使用 `array[index] = value` 语法在某个索引位置设置值,使用 `array[index]` 获取值。 +--- +内置函数 `len` 返回数组的长度。 +--- +使用这种语法可以在一行中声明并初始化一个数组。 +--- +如果你不想手动写出数组的长度,可以使用这种方式,让编译器自动计算数组的长度。 diff --git a/locales/zh/116-Arrays/arrays2.md b/locales/zh/116-Arrays/arrays2.md new file mode 100644 index 0000000..dbc4c18 --- /dev/null +++ b/locales/zh/116-Arrays/arrays2.md @@ -0,0 +1,5 @@ +### 多维数组的声明 +--- +数组类型是一维的,但你可以组合类型来构建多维数据结构。 +--- +如果需要声明更多维度,可以自行扩展,例如声明一个 2*2*3 的三维数组 diff --git a/locales/zh/116-Arrays/arrays3.md b/locales/zh/116-Arrays/arrays3.md new file mode 100644 index 0000000..6336aea --- /dev/null +++ b/locales/zh/116-Arrays/arrays3.md @@ -0,0 +1,7 @@ +#### 提示 +--- +如果在 XGo 中不声明数组的内容,编译器会自动将数组初始化为 0;对于 bool 类型的数组,初始值为 false。 +--- +关于更多数组值类型,XGo 支持字符串、整数、浮点数、布尔值等。详情请参阅本章: +--- +* https://tutorial.xgo.dev/values diff --git a/locales/zh/117-Slices/slices-01.md b/locales/zh/117-Slices/slices-01.md new file mode 100644 index 0000000..ff9df25 --- /dev/null +++ b/locales/zh/117-Slices/slices-01.md @@ -0,0 +1,4 @@ +### XGo 风格的切片字面量 +切片是相同类型数据元素的集合。切片字面量是由方括号包围的表达式列表。可以使用索引表达式访问单个元素。索引从 0 开始。 + +在 XGo 中,你可以直接通过 len 方法获取切片长度,也可以对切片字面量进行类型转换: diff --git a/locales/zh/117-Slices/slices-02.md b/locales/zh/117-Slices/slices-02.md new file mode 100644 index 0000000..474b5b3 --- /dev/null +++ b/locales/zh/117-Slices/slices-02.md @@ -0,0 +1,6 @@ +### 切片 +数组具有固定大小。而切片是对数组元素的动态大小、灵活的视图。在实践中,切片比数组更常用。 + +类型 []T 是一个元素类型为 T 的切片。 + +切片通过指定两个索引(上界和下界)来形成,两者之间用冒号分隔:a[low : high]。它选择一个半开区间,包含第一个元素,但不包含最后一个元素。 diff --git a/locales/zh/117-Slices/slices-03.md b/locales/zh/117-Slices/slices-03.md new file mode 100644 index 0000000..95c2c78 --- /dev/null +++ b/locales/zh/117-Slices/slices-03.md @@ -0,0 +1,6 @@ +### 切片类似于数组的引用 +切片不存储任何数据,它只是描述了底层数组的一个片段。 + +修改切片的元素会修改其底层数组中对应的元素。 + +共享同一底层数组的其他切片会看到这些更改。 diff --git a/locales/zh/117-Slices/slices-04.md b/locales/zh/117-Slices/slices-04.md new file mode 100644 index 0000000..f52cce6 --- /dev/null +++ b/locales/zh/117-Slices/slices-04.md @@ -0,0 +1,8 @@ +### 切片字面量 +切片字面量类似于没有长度的数组字面量。 +
+这是一个数组字面量:
+[3]bool{true, true, false}
+而下面这种写法会创建与上面相同的数组,然后构建一个引用该数组的切片:
+[]bool{true, true, false}
+
diff --git a/locales/zh/117-Slices/slices-05.md b/locales/zh/117-Slices/slices-05.md new file mode 100644 index 0000000..9c5ba80 --- /dev/null +++ b/locales/zh/117-Slices/slices-05.md @@ -0,0 +1,13 @@ +### 切片的默认值 +在进行切片操作时,可以省略上界或下界来使用其默认值。 + +下界的默认值是 0,上界的默认值是切片的长度。 +
+对于数组
+var a [10]int
+以下切片表达式是等价的:
+a[0:10]
+a[:10]
+a[0:]
+a[:]
+
diff --git a/locales/zh/117-Slices/slices-06.md b/locales/zh/117-Slices/slices-06.md new file mode 100644 index 0000000..809088e --- /dev/null +++ b/locales/zh/117-Slices/slices-06.md @@ -0,0 +1,10 @@ +### 切片的长度和容量 +切片同时具有长度和容量。 + +切片的长度是它所包含的元素数量。 + +切片的容量是从切片的第一个元素开始计算,到底层数组末尾的元素数量。 + +切片 s 的长度和容量可以通过表达式 len(s) 和 cap(s) 来获取。 + +你可以通过重新切片来扩展切片的长度,前提是它有足够的容量。试着修改示例程序中的某个切片操作,使其超出容量,看看会发生什么。 diff --git a/locales/zh/117-Slices/slices-07.md b/locales/zh/117-Slices/slices-07.md new file mode 100644 index 0000000..9072ae1 --- /dev/null +++ b/locales/zh/117-Slices/slices-07.md @@ -0,0 +1,4 @@ +### nil 切片 +切片的零值是 nil。 + +nil 切片的长度和容量都是 0,并且没有底层数组。 diff --git a/locales/zh/117-Slices/slices-08.md b/locales/zh/117-Slices/slices-08.md new file mode 100644 index 0000000..52b8721 --- /dev/null +++ b/locales/zh/117-Slices/slices-08.md @@ -0,0 +1,12 @@ +### 使用 make 创建切片 +切片可以使用内置的 make 函数创建;这是创建动态大小数组的方式。 + +make 函数会分配一个零值数组,并返回一个引用该数组的切片: + +
+a := make([]int, 5)			// len(a)=5
+要指定容量,可以向 make 传递第三个参数:
+b := make([]int, 0, 5) 		// len(b)=0, cap(b)=5
+b = b[:cap(b)] 				// len(b)=5, cap(b)=5
+b = b[1:]      				// len(b)=4, cap(b)=4
+
diff --git a/locales/zh/117-Slices/slices-09.md b/locales/zh/117-Slices/slices-09.md new file mode 100644 index 0000000..36107a1 --- /dev/null +++ b/locales/zh/117-Slices/slices-09.md @@ -0,0 +1,3 @@ +### 切片的切片 +切片可以包含任何类型,包括其他切片。 +创建一个井字棋棋盘。 diff --git a/locales/zh/117-Slices/slices-10.md b/locales/zh/117-Slices/slices-10.md new file mode 100644 index 0000000..24b743c --- /dev/null +++ b/locales/zh/117-Slices/slices-10.md @@ -0,0 +1,10 @@ +### 向切片追加元素 +向切片追加新元素是很常见的操作,因此 Go 提供了内置的 append 函数。内置包的文档中描述了 append 的用法。 + +func append(s []T, vs ...T) []T + +append 的第一个参数 s 是一个类型为 T 的切片,其余参数是要追加到切片中的 T 类型的值。 + +append 的返回值是一个切片,包含原始切片的所有元素加上新提供的值。 + +如果 s 的底层数组太小,无法容纳所有给定的值,则会分配一个更大的数组。返回的切片将指向新分配的数组。 diff --git a/locales/zh/117-Slices/slices-11.md b/locales/zh/117-Slices/slices-11.md new file mode 100644 index 0000000..ac13c28 --- /dev/null +++ b/locales/zh/117-Slices/slices-11.md @@ -0,0 +1,4 @@ +### Range +for 循环的 range 形式可以遍历切片或 map。 + +在遍历切片时,每次迭代会返回两个值。第一个是索引,第二个是该索引处元素的副本。 diff --git a/locales/zh/117-Slices/slices-12.md b/locales/zh/117-Slices/slices-12.md new file mode 100644 index 0000000..830fe29 --- /dev/null +++ b/locales/zh/117-Slices/slices-12.md @@ -0,0 +1,9 @@ +### Range 续 + +
+你可以通过赋值给 _ 来跳过索引或值。
+for i, _ := range pow
+for _, value := range pow
+如果只需要索引,可以省略第二个变量。
+for i := range pow
+
diff --git a/locales/zh/118-Maps/map-0.md b/locales/zh/118-Maps/map-0.md new file mode 100644 index 0000000..d51d81b --- /dev/null +++ b/locales/zh/118-Maps/map-0.md @@ -0,0 +1,17 @@ +_Maps_ 是 XGo 内置的[关联数据类型](http://en.wikipedia.org/wiki/Associative_array)(在其他语言中有时称为 _哈希_ 或 _字典_)。 +--- +### Map 基础 +--- +使用 `make(map[key-type]val-type)` 创建一个空的 map。 +--- +使用常见的 `name[key] = val` 语法设置键值对。 +--- +使用例如 `println` 打印 map 时,会显示其所有的键值对。 +--- +使用 `name[key]` 获取某个键对应的值。 +--- +内置函数 `len` 在用于 map 时,返回键值对的数量。 +--- +内置函数 `delete` 从 map 中删除键值对。 +--- +从 map 中获取值时,可选的第二个返回值指示该键是否存在于 map 中。这可以用来区分缺失的键和零值的键(如 `0` 或 `""`)。这里我们不需要值本身,所以用 _空白标识符_ `_` 忽略了它。 diff --git a/locales/zh/118-Maps/map-1.md b/locales/zh/118-Maps/map-1.md new file mode 100644 index 0000000..445e0b8 --- /dev/null +++ b/locales/zh/118-Maps/map-1.md @@ -0,0 +1,11 @@ +### XGo 风格的 Map 字面量 +--- +map[string]int +--- +map[string]float64 +--- +map[string]interface{} +--- +map[int]interface{} +--- +空 map 的类型是 map[string]interface{} diff --git a/locales/zh/118-Maps/map-2.md b/locales/zh/118-Maps/map-2.md new file mode 100644 index 0000000..31ddc7c --- /dev/null +++ b/locales/zh/118-Maps/map-2.md @@ -0,0 +1,13 @@ +### Go 风格的 Map 字面量 +--- +你也可以使用以下语法在同一行中声明并初始化一个新的 map。 +--- +{"Hello": 1, "xsw": 3} +--- +{"Hello": 1, "xsw": 3.4} +--- +{"Hello": 1, "xsw": "XGo"} +--- +{1: 1.4, 3: "XGo"} +--- +{} diff --git a/locales/zh/119-Structs/struct-1.md b/locales/zh/119-Structs/struct-1.md new file mode 100644 index 0000000..333ca08 --- /dev/null +++ b/locales/zh/119-Structs/struct-1.md @@ -0,0 +1,9 @@ +Map 是存储某些类型数据的便捷方式,但它有局限性。它不能定义 API,因为无法限制 map 只允许特定的键。map 中的所有值也必须是相同的类型。因此,map 不是在函数之间传递数据的理想方式。当你有需要组合在一起的相关数据时,应该定义一个结构体。 + +结构体类型使用关键字 type、结构体类型名称、关键字 struct 和一对花括号({})来定义。在花括号内,列出结构体的字段。就像在 var 声明中先写变量名再写变量类型一样,结构体字段也是先写字段名再写字段类型。 + +还需要注意的是,与 map 字面量不同,结构体声明中字段之间没有逗号分隔。你可以在函数内部或外部定义结构体类型。在函数内部定义的结构体类型只能在该函数内使用。从技术上讲,你可以在任何块级别定义结构体。 + +一旦声明了结构体类型,我们就可以定义该类型的变量。 +--- +这里我们使用了 var 声明。由于没有给 fred 赋值,它会获得 person 结构体类型的零值。零值结构体的每个字段都被设置为该字段的零值。 diff --git a/locales/zh/119-Structs/struct-2.md b/locales/zh/119-Structs/struct-2.md new file mode 100644 index 0000000..f9dac9a --- /dev/null +++ b/locales/zh/119-Structs/struct-2.md @@ -0,0 +1,4 @@ +### 非空结构体字面量有两种不同的风格。 +结构体字面量可以指定为花括号内以逗号分隔的字段值列表: +--- +使用这种结构体字面量格式时,必须为结构体中的每个字段指定一个值,并且值按照它们在结构体定义中声明的顺序赋给字段。 diff --git a/locales/zh/119-Structs/struct-3.md b/locales/zh/119-Structs/struct-3.md new file mode 100644 index 0000000..28932c3 --- /dev/null +++ b/locales/zh/119-Structs/struct-3.md @@ -0,0 +1,4 @@ + +第二种结构体字面量风格类似于 map 字面量风格: +--- +你可以使用结构体中字段的名称来指定值。使用这种风格时,你可以省略某些键并以任意顺序指定字段。未指定的字段会被设置为其零值。你不能混合使用两种结构体字面量风格;要么所有字段都使用键名指定,要么都不使用。对于所有字段总是被指定的小型结构体,使用较简单的结构体字面量风格即可。在其他情况下,使用键名方式。虽然更冗长,但它能清楚地表明哪个值被赋给了哪个字段,而无需参考结构体定义。这种方式也更易于维护。如果你在初始化结构体时没有使用字段名,而未来版本的结构体添加了额外的字段,你的代码将无法编译。 diff --git a/locales/zh/119-Structs/struct-4.md b/locales/zh/119-Structs/struct-4.md new file mode 100644 index 0000000..35afd86 --- /dev/null +++ b/locales/zh/119-Structs/struct-4.md @@ -0,0 +1,4 @@ + +结构体中的字段使用点号表示法来访问: +--- +就像我们使用方括号来读写 map 一样,我们使用点号表示法来读写结构体字段。 diff --git a/locales/zh/119-Structs/struct-5.md b/locales/zh/119-Structs/struct-5.md new file mode 100644 index 0000000..1a7e737 --- /dev/null +++ b/locales/zh/119-Structs/struct-5.md @@ -0,0 +1,6 @@ +### 匿名结构体 +你也可以声明一个变量实现某个结构体类型,而不必先给该结构体类型命名。这被称为匿名结构体。 +--- +在这个示例中,变量 person 和 pet 的类型是匿名结构体。你可以像对命名结构体类型一样,对匿名结构体进行字段的赋值和读取。就像你可以用结构体字面量初始化一个命名结构体的实例一样,你也可以对匿名结构体做同样的事情。你可能会想,什么时候需要一个只与单个实例关联的数据类型。有两种常见的情况适合使用匿名结构体。第一种是当你将外部数据转换为结构体,或将结构体转换为外部数据时(如 JSON 或 protocol buffers)。这被称为数据的序列化和反序列化。 + +编写测试是匿名结构体的另一个常见使用场景。 diff --git a/locales/zh/119-Structs/struct-6.md b/locales/zh/119-Structs/struct-6.md new file mode 100644 index 0000000..47e2f28 --- /dev/null +++ b/locales/zh/119-Structs/struct-6.md @@ -0,0 +1,9 @@ +### 结构体的比较和转换 +结构体是否可比较取决于其字段。完全由可比较类型组成的结构体是可比较的;包含切片或 map 字段的结构体则不可比较(正如我们将在后续章节中看到的,函数和通道字段也会使结构体不可比较)。 + +与 Python 或 Ruby 不同,没有可以重写的魔法方法来重新定义相等性,使 == 和 != 适用于不可比较的结构体。当然,你可以编写自己的函数来比较结构体。 + +就像 XGo 不允许在不同基本类型的变量之间进行比较一样,XGo 也不允许在表示不同类型结构体的变量之间进行比较。如果两个结构体的字段具有相同的名称、顺序和类型,XGo 允许你执行从一个结构体类型到另一个结构体类型的类型转换。让我们看看这意味着什么。给定以下结构体: +--- +我们可以使用类型转换将 secondPerson 的实例转换为 firstPerson,但不能将 thirdPerson 的实例转换为 firstPerson,因为字段的顺序不同。 +但我们不能使用 == 来比较 firstPerson 的实例和 secondPerson 或 thirdPerson 的实例,因为它们是不同的类型。 diff --git a/locales/zh/119-Structs/struct-7.md b/locales/zh/119-Structs/struct-7.md new file mode 100644 index 0000000..ca9c2b4 --- /dev/null +++ b/locales/zh/119-Structs/struct-7.md @@ -0,0 +1 @@ +匿名结构体在这方面有一个小的特殊之处:如果正在比较的两个结构体变量中至少有一个是匿名结构体类型,那么只要两个结构体的字段具有相同的名称、顺序和类型,就可以在不进行类型转换的情况下比较它们。同样,如果两个结构体的字段具有相同的名称、顺序和类型,你也可以在命名结构体类型和匿名结构体类型之间进行赋值。 diff --git a/locales/zh/120-Pointers/pointer-1.md b/locales/zh/120-Pointers/pointer-1.md new file mode 100644 index 0000000..1c7e026 --- /dev/null +++ b/locales/zh/120-Pointers/pointer-1.md @@ -0,0 +1,6 @@ +指针是一个值为内存地址的变量。指针使用取地址符(& 字符)定义,即地址运算符,后跟变量名。 + +指针的类型是固定的,这意味着当你创建一个指向 int 的指针时,你可以改变它所指向的值,但不能用它指向存储不同类型(如 float64)的内存地址。这个限制很重要,指针不仅仅是内存地址,而是可能存储特定类型值的内存地址。 + +指针的类型基于创建它的变量类型,前面加上星号(* 字符)。名为 second 的变量类型是 *int,因为它是通过对 first 变量应用地址运算符创建的,而 first 的值是 int 类型。当你看到类型 *int 时,你就知道这是一个值为存储 int 变量的内存地址的变量。 + diff --git a/locales/zh/120-Pointers/pointer-2.md b/locales/zh/120-Pointers/pointer-2.md new file mode 100644 index 0000000..d326ade --- /dev/null +++ b/locales/zh/120-Pointers/pointer-2.md @@ -0,0 +1,6 @@ +### 跟随指针 +"跟随指针"这个短语的意思是读取指针所引用的内存地址处的值,这是通过使用星号(* 字符)来完成的。星号告诉 XGo 跟随指针并获取该内存位置的值。这被称为解引用指针。 +--- +第一条新语句定义了一个新变量,这里使用 var 关键字来强调变量类型是 *int,即指向 int 值的指针。下一条语句将 second 变量的值赋给新变量,这意味着 second 和 myNewPointer 的值都是 first 值的内存位置。跟随任一指针都会访问相同的内存位置,这意味着递增 myNewPointer 会影响通过跟随 second 指针获得的值。 + +一个常见的误解是 first 和 second 变量具有相同的值,但事实并非如此。这里有两个值。一个是可以通过名为 first 的变量访问的 int 值。还有一个 *int 值,它存储了 first 值的内存位置。可以跟随 *int 值来访问存储的 int 值。但是,因为 *int 值本身也是一个值,所以它可以独立使用,这意味着它可以赋值给其他变量、用作调用函数的参数等。 diff --git a/locales/zh/120-Pointers/pointer-3.md b/locales/zh/120-Pointers/pointer-3.md new file mode 100644 index 0000000..41887f5 --- /dev/null +++ b/locales/zh/120-Pointers/pointer-3.md @@ -0,0 +1,6 @@ +### 理解指针的零值 +已定义但未赋值的指针的零值为 nil。 + +指针 second 已定义但未初始化值,并使用 println 函数输出。然后使用地址运算符创建指向 first 变量的指针,再次输出 second 的值。 + +如果你跟随一个未被赋值的指针,将会发生运行时错误。 diff --git a/locales/zh/120-Pointers/pointer-4.md b/locales/zh/120-Pointers/pointer-4.md new file mode 100644 index 0000000..9b55d45 --- /dev/null +++ b/locales/zh/120-Pointers/pointer-4.md @@ -0,0 +1,4 @@ +### 指向指针的指针 +由于指针存储内存位置,因此可以创建一个值为另一个指针内存地址的指针。 +--- +跟随指针链的语法可能比较笨拙。在这种情况下,需要两个星号。第一个星号跟随指针到内存位置,获取名为 second 的变量存储的值,即一个 *int 值。第二个星号跟随名为 second 的指针,从而访问 first 变量存储的值的内存位置。这不是你在大多数项目中需要做的事情,但它很好地确认了指针的工作方式以及如何沿着链路获取数据值。 diff --git a/locales/zh/121-For-Each/for-each-1.md b/locales/zh/121-For-Each/for-each-1.md new file mode 100644 index 0000000..3a6e686 --- /dev/null +++ b/locales/zh/121-For-Each/for-each-1.md @@ -0,0 +1 @@ +在 XGo 中没有 foreach 循环,但 for 循环可以用作 "foreach"。有一个关键字 range,你可以将 for 和 range 组合在一起,并可以选择在循环中使用键或值。 diff --git a/locales/zh/121-For-Each/for-each-2.md b/locales/zh/121-For-Each/for-each-2.md new file mode 100644 index 0000000..5c14db6 --- /dev/null +++ b/locales/zh/121-For-Each/for-each-2.md @@ -0,0 +1,6 @@ +### for-range 语句 +for-range 循环的有趣之处在于你会获得两个循环变量。第一个变量是正在迭代的数据结构中的位置,第二个是该位置的值。两个循环变量的惯用名称取决于正在遍历的内容。遍历数组、切片或字符串时,通常使用 i 表示索引。遍历 map 时,则使用 k(表示键)。 + +第二个变量通常被命名为 v 表示值,但有时也会根据被迭代值的类型来命名。 + +如果你不需要访问键,可以使用下划线(_)作为变量名。这告诉 XGo 忽略该值。 diff --git a/locales/zh/121-For-Each/for-each-3.md b/locales/zh/121-For-Each/for-each-3.md new file mode 100644 index 0000000..9aa4c86 --- /dev/null +++ b/locales/zh/121-For-Each/for-each-3.md @@ -0,0 +1,2 @@ +### 遍历 Map +for-range 循环遍历 map 的方式有一些有趣的地方。 diff --git a/locales/zh/121-For-Range/range.md b/locales/zh/121-For-Range/range.md new file mode 100644 index 0000000..9131c76 --- /dev/null +++ b/locales/zh/121-For-Range/range.md @@ -0,0 +1,11 @@ +_range_ 可以遍历多种数据结构中的元素。让我们看看如何将 `range` 与我们已经学过的一些数据结构一起使用。 +--- +这里我们使用 `range` 对切片中的数字求和。数组也可以这样使用。 +--- +对数组和切片使用 `range` 时,会同时提供每个条目的索引和值。上面我们不需要索引,所以用空白标识符 `_` 忽略了它。但有时我们确实需要索引。 +--- +对 map 使用 `range` 会遍历键值对。 +--- +`range` 也可以只遍历 map 的键。 +--- +对字符串使用 `range` 会遍历 Unicode 码点。第一个值是 `rune` 的起始字节索引,第二个值是 `rune` 本身。 diff --git a/locales/zh/121-List-Comprehension/listcompr.md b/locales/zh/121-List-Comprehension/listcompr.md new file mode 100644 index 0000000..1afd840 --- /dev/null +++ b/locales/zh/121-List-Comprehension/listcompr.md @@ -0,0 +1,9 @@ +生成奇数和偶数列表。 +--- +平方数 +--- +迭代 +--- +分割字符串 +--- +按指定字符分割字符串 diff --git a/locales/zh/201-Functions/funcs.md b/locales/zh/201-Functions/funcs.md new file mode 100644 index 0000000..68eb001 --- /dev/null +++ b/locales/zh/201-Functions/funcs.md @@ -0,0 +1,9 @@ +_函数_ 是 XGo 的核心。我们将通过几个不同的示例来学习函数。 +--- +这是一个接受两个 `int` 参数并返回它们之和(`int` 类型)的函数。 +--- +XGo 要求显式返回,即不会自动返回最后一个表达式的值。 +--- +当多个连续参数具有相同类型时,可以省略前面同类型参数的类型名称,只在最后一个参数处声明类型。 +--- +调用函数的方式与你预期的一样,使用 `name(args)`。 diff --git a/locales/zh/202-Multiple-Return-Values/multi-rets.md b/locales/zh/202-Multiple-Return-Values/multi-rets.md new file mode 100644 index 0000000..e85c9f0 --- /dev/null +++ b/locales/zh/202-Multiple-Return-Values/multi-rets.md @@ -0,0 +1,7 @@ +XGo 内置支持_多返回值_。这个特性在惯用的 XGo 代码中经常使用,例如同时返回函数的结果和错误值。 +--- +该函数签名中的 `(int, int)` 表示函数返回两个 `int` 值。 +--- +这里我们通过_多重赋值_来使用调用返回的两个不同值。 +--- +如果你只需要返回值的一部分,可以使用空白标识符 `_`。 diff --git a/locales/zh/203-Errors/errors.md b/locales/zh/203-Errors/errors.md new file mode 100644 index 0000000..334241a --- /dev/null +++ b/locales/zh/203-Errors/errors.md @@ -0,0 +1,15 @@ +在 XGo 中,惯用的做法是通过一个显式的、独立的返回值来传达错误。这与 Java 和 Ruby 等语言使用异常的方式不同,也不同于 C 语言中有时使用的重载单一结果/错误值的做法。XGo 的方式使得很容易看出哪些函数会返回错误,并使用与处理其他非错误任务相同的语言结构来处理它们。 +--- +按照惯例,错误是最后一个返回值,类型为 `error`,这是一个内置接口。 +--- +`errors.New` 使用给定的错误消息构造一个基本的 `error` 值。 +--- +错误位置的 `nil` 值表示没有错误。 +--- +可以通过在自定义类型上实现 `Error()` 方法来将其用作 `error`。以下是上面示例的一个变体,使用自定义类型来显式表示参数错误。 +--- +在这个例子中,我们使用 `&argError` 语法来构建一个新的结构体,为 `arg` 和 `prob` 两个字段提供值。 +--- +下面的两个循环测试了我们每个返回错误的函数。注意在 `if` 行中使用内联错误检查是 XGo 代码中的常见惯用法。 +--- +如果你想以编程方式使用自定义错误中的数据,需要通过类型断言将错误获取为自定义错误类型的实例。 diff --git a/locales/zh/204-Function Values/func-values.md b/locales/zh/204-Function Values/func-values.md new file mode 100644 index 0000000..c7d6146 --- /dev/null +++ b/locales/zh/204-Function Values/func-values.md @@ -0,0 +1 @@ +函数也是值。它们可以像其他值一样被传递。函数值可以用作函数参数和返回值。 diff --git a/locales/zh/205-Closures/closures.md b/locales/zh/205-Closures/closures.md new file mode 100644 index 0000000..08255b0 --- /dev/null +++ b/locales/zh/205-Closures/closures.md @@ -0,0 +1 @@ +XGo 函数可以是闭包。闭包是一个引用了其函数体外部变量的函数值。该函数可以访问和赋值所引用的变量;从这个意义上说,函数被"绑定"到了这些变量上。例如,adder 函数返回一个闭包。每个闭包都绑定到它自己的 sum 变量。 diff --git a/locales/zh/205-Lambda-expressions/lambda-expressions-1.md b/locales/zh/205-Lambda-expressions/lambda-expressions-1.md new file mode 100644 index 0000000..dd05a71 --- /dev/null +++ b/locales/zh/205-Lambda-expressions/lambda-expressions-1.md @@ -0,0 +1,3 @@ +Lambda 表达式用于在不给函数命名的情况下内联定义函数。 +--- +以下示例展示了 XGo 风格的 lambda 表达式,它更紧凑且易于理解。 diff --git a/locales/zh/205-Lambda-expressions/lambda-expressions-2.md b/locales/zh/205-Lambda-expressions/lambda-expressions-2.md new file mode 100644 index 0000000..9981239 --- /dev/null +++ b/locales/zh/205-Lambda-expressions/lambda-expressions-2.md @@ -0,0 +1 @@ +如果我们想定义一个 lambda 但不立即执行它,只需省略其标识符即可。例如: diff --git a/locales/zh/205-Lambda-expressions/lambda-expressions-3.md b/locales/zh/205-Lambda-expressions/lambda-expressions-3.md new file mode 100644 index 0000000..054519b --- /dev/null +++ b/locales/zh/205-Lambda-expressions/lambda-expressions-3.md @@ -0,0 +1 @@ +如果我们想定义 lambda 并立即执行它,需要省略其标识符,并在函数体的右花括号后面添加一个带括号的参数列表。例如: diff --git a/locales/zh/206-Recursion/recursion.md b/locales/zh/206-Recursion/recursion.md new file mode 100644 index 0000000..27d9d65 --- /dev/null +++ b/locales/zh/206-Recursion/recursion.md @@ -0,0 +1,6 @@ +XGo 编程语言支持递归,即允许函数调用自身。但在使用递归时,程序员需要注意定义函数的退出条件,否则将进入无限循环。 +递归函数在解决许多数学问题时非常有用,例如计算阶乘、生成斐波那契数列等。 +--- +本示例使用递归函数计算给定数字的阶乘 +--- +本示例展示如何使用递归函数生成给定数字的斐波那契数列 diff --git a/locales/zh/207-Variadic-Parameters/variadic.md b/locales/zh/207-Variadic-Parameters/variadic.md new file mode 100644 index 0000000..ff57c8f --- /dev/null +++ b/locales/zh/207-Variadic-Parameters/variadic.md @@ -0,0 +1 @@ +以可变数量参数调用的 joinstr 函数被称为可变参数函数。在 joinstr 函数的声明中,最后一个参数的类型前面带有省略号,即 (…)。这表示该函数可以接受任意数量的 string 参数。调用 joinstr(elements...) 时,如果不加 ...,将无法编译,因为类型不匹配;elements 不是 string 类型。 diff --git a/locales/zh/208-Defer/defer-1.md b/locales/zh/208-Defer/defer-1.md new file mode 100644 index 0000000..7f9dbfa --- /dev/null +++ b/locales/zh/208-Defer/defer-1.md @@ -0,0 +1,3 @@ +defer 语句会将函数的执行推迟到外层函数返回时。被推迟调用的函数参数会立即求值,但函数调用直到外层函数返回时才会执行。Defer 通常用于简化执行各种清理操作的函数。 +--- +defer 语句允许我们在打开文件后立即考虑关闭它,从而保证无论函数中有多少个 return 语句,文件都会被关闭。 diff --git a/locales/zh/208-Defer/defer-2.md b/locales/zh/208-Defer/defer-2.md new file mode 100644 index 0000000..b090940 --- /dev/null +++ b/locales/zh/208-Defer/defer-2.md @@ -0,0 +1,3 @@ +### defer 堆叠 +--- +被推迟的函数调用会被压入一个栈中。当函数返回时,其推迟的调用会按照后进先出的顺序执行。 diff --git a/locales/zh/209-Exceptions/exceptions-1.md b/locales/zh/209-Exceptions/exceptions-1.md new file mode 100644 index 0000000..e3a56f5 --- /dev/null +++ b/locales/zh/209-Exceptions/exceptions-1.md @@ -0,0 +1,5 @@ +### Panic +Panic 是一个内置函数,用于停止正常的执行流程。当你在代码中调用 panic 时,意味着你已经判定调用者无法解决该问题。因此,你应该仅在代码或集成你代码的人在该点继续执行不安全的极少数情况下使用 panic。 +以下代码示例演示了 panic 的工作方式: +--- +如上所示,当使用 panic 且未被处理时,执行流程将停止,所有被推迟的函数按相反顺序执行,并打印堆栈跟踪信息。 diff --git a/locales/zh/209-Exceptions/exceptions-2.md b/locales/zh/209-Exceptions/exceptions-2.md new file mode 100644 index 0000000..d393411 --- /dev/null +++ b/locales/zh/209-Exceptions/exceptions-2.md @@ -0,0 +1,5 @@ +### Recover +--- +要将错误作为返回值报告,你必须在调用 panic 函数的同一个 goroutine 中调用 recover 函数,从 recover 函数获取错误结构体,并将其传递给一个变量: +--- +每个被推迟的函数都会在函数调用之后、return 语句之前执行。因此,你可以在 return 语句执行之前设置返回变量的值。 diff --git a/locales/zh/210-Methods/methods-1.md b/locales/zh/210-Methods/methods-1.md new file mode 100644 index 0000000..cdfb8ee --- /dev/null +++ b/locales/zh/210-Methods/methods-1.md @@ -0,0 +1 @@ +XGo 没有类。但是,你可以在类型上定义方法。方法是一个带有特殊接收者参数的函数。接收者出现在 func 关键字和方法名之间的参数列表中。 diff --git a/locales/zh/210-Methods/methods-2.md b/locales/zh/210-Methods/methods-2.md new file mode 100644 index 0000000..3b80d19 --- /dev/null +++ b/locales/zh/210-Methods/methods-2.md @@ -0,0 +1,3 @@ +### 在非结构体类型上声明方法 +--- +在这个示例中,我们看到一个数值类型 MyFloat 带有一个 Abs 方法。你只能在与方法定义在同一个包中的类型上声明带接收者的方法。你不能在其他包中定义的类型上声明方法(包括 int 等内置类型)。 diff --git a/locales/zh/211-Methods-with-a-Pointer-Receiver/ptr-methods-1.md b/locales/zh/211-Methods-with-a-Pointer-Receiver/ptr-methods-1.md new file mode 100644 index 0000000..0cd9cd7 --- /dev/null +++ b/locales/zh/211-Methods-with-a-Pointer-Receiver/ptr-methods-1.md @@ -0,0 +1,11 @@ +### 指针接收者 +--- +你可以声明带指针接收者的方法。 +--- +这意味着接收者类型具有 *T 的字面语法,其中 T 是某个类型。(另外,T 本身不能是指针类型,如 *int。) +--- +例如,这里的 Scale 方法定义在 *Vertex 上。 +--- +带指针接收者的方法可以修改接收者所指向的值(如 Scale 所做的那样)。由于方法经常需要修改其接收者,因此指针接收者比值接收者更常用。 +--- +使用值接收者时,Scale 方法操作的是原始 Vertex 值的副本。(这与其他函数参数的行为相同。)Scale 方法必须使用指针接收者才能修改 Vertex 的值。 diff --git a/locales/zh/212-Composing-Types-by-Struct-Embedding/struct-emb-1.md b/locales/zh/212-Composing-Types-by-Struct-Embedding/struct-emb-1.md new file mode 100644 index 0000000..fb284f0 --- /dev/null +++ b/locales/zh/212-Composing-Types-by-Struct-Embedding/struct-emb-1.md @@ -0,0 +1,9 @@ +XGo 不提供典型的、基于类型驱动的子类化概念,但它可以通过在结构体或接口中嵌入类型来"借用"部分实现。只有接口可以嵌入到接口中。 +--- +示例中有两个结构体类型,Hello 结构体和 Goodbye 结构体,它们各自实现了 Talk 接口。HelloGoodbye 结构体也实现了 Talk 接口,它通过嵌入的方式将 Hello 结构体和 Goodbye 结构体组合到一个结构体中:在结构体中列出类型但不给它们字段名。 +--- +嵌入的元素是指向结构体的指针,当然在使用之前必须初始化为指向有效的结构体。 +--- +HelloGoodbye 结构体也有一个写作 forward *Forward 的 forward 成员,但如果要提升 forward 的方法并满足 Talk 接口,我们还需要提供转发方法,例如:hg.forward.Say()。通过直接嵌入结构体,我们避免了这种繁琐的操作。嵌入类型的方法会自动继承过来,这意味着 HelloGoodbye 结构体也拥有 Hello 结构体和 Goodbye 结构体的方法。 +--- +嵌入与子类化之间有一个重要区别。当我们嵌入一个类型时,该类型的方法成为外部类型的方法,但当它们被调用时,方法的接收者是内部类型,而不是外部类型。在我们的示例中,当 HelloGoodbye 的 Sleep 方法被调用时,它的效果与转发方法 helloGoodbye.Hello.Sleep() 完全相同;接收者是 helloGoodbye.Hello,而不是 helloGoodbye 本身。 diff --git a/locales/zh/212-Composing-Types-by-Struct-Embedding/struct-emb-2.md b/locales/zh/212-Composing-Types-by-Struct-Embedding/struct-emb-2.md new file mode 100644 index 0000000..3135f4e --- /dev/null +++ b/locales/zh/212-Composing-Types-by-Struct-Embedding/struct-emb-2.md @@ -0,0 +1,12 @@ +### 嵌入也可以是一种简单的便利方式。 +本示例展示了一个嵌入字段与一个常规的命名字段并列使用。 +--- +Job 类型现在拥有 *log.Logger 的 Print、Printf、Println 等方法。当然,我们可以给 Logger 一个字段名,但没有必要这样做。初始化之后,我们就可以向 Job 记录日志:job.Println("starting now...") +--- +Logger 是 Job 结构体的一个常规字段,因此我们可以在 Job 的构造函数中以通常的方式初始化它,就像 NewJob 函数所做的那样,或者使用复合字面量,job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)} +--- +如果我们需要直接引用嵌入字段,忽略包限定符后的类型名就可以作为字段名使用,就像我们 Job 结构体的 Printf 方法中那样。这里,如果我们需要访问 Job 变量 job 的 *log.Logger,我们可以写 job.Logger,这在我们想要改进 Logger 方法时很有用。 +--- +嵌入类型会引入名称冲突的问题,但解决规则很简单。首先,字段或方法 X 会隐藏类型中更深层嵌套部分的任何其他项 X。如果 log.Logger 包含一个名为 Command 的字段或方法,Job 的 Command 字段将优先使用。 +--- +其次,如果相同的名称出现在相同的嵌套层级,这通常是一个错误;如果 Job 结构体包含另一个名为 Logger 的字段或方法,那么嵌入 log.Logger 将是错误的。但是,如果重复的名称在类型定义之外的程序中从未被提及,则是允许的。这一限定为防止从外部嵌入的类型发生更改提供了一定的保护;如果添加的字段与另一个子类型中的字段冲突,只要两个字段都没有被使用过,就不会有问题。 diff --git a/locales/zh/213-Method-Values-and-Expressions/method-values-1.md b/locales/zh/213-Method-Values-and-Expressions/method-values-1.md new file mode 100644 index 0000000..97354a5 --- /dev/null +++ b/locales/zh/213-Method-Values-and-Expressions/method-values-1.md @@ -0,0 +1,5 @@ +### 方法值 +--- +方法值允许你将方法绑定到特定的对象,然后像普通函数一样调用该方法,对象被隐含在内,类似于闭包。例如: +--- +表达式 hello.Say 创建了一个方法值,将 Say 函数绑定到特定的变量 hello,使其类型为 func()。因此函数值可以作为函数参数传递。 diff --git a/locales/zh/213-Method-Values-and-Expressions/method-values-2.md b/locales/zh/213-Method-Values-and-Expressions/method-values-2.md new file mode 100644 index 0000000..2f0fa97 --- /dev/null +++ b/locales/zh/213-Method-Values-and-Expressions/method-values-2.md @@ -0,0 +1,5 @@ +### 方法表达式 +--- +方法表达式可以将方法转换为一个以接收者作为第一个参数的函数。例如: +--- +因此,如果你定义了一个 Point 结构体和一个方法 func (p Point) Add(another Point),你可以写 Point.Add 来获得一个 func(p Point, another Point)。 diff --git a/locales/zh/213-Method-Values-and-Expressions/method-values-3.md b/locales/zh/213-Method-Values-and-Expressions/method-values-3.md new file mode 100644 index 0000000..b0fd017 --- /dev/null +++ b/locales/zh/213-Method-Values-and-Expressions/method-values-3.md @@ -0,0 +1 @@ +但如果方法有指针接收者,你需要在方法表达式中写成 (*Type).Method。例如: diff --git a/locales/zh/214-Encapsulation/encap-1.md b/locales/zh/214-Encapsulation/encap-1.md new file mode 100644 index 0000000..b02cf97 --- /dev/null +++ b/locales/zh/214-Encapsulation/encap-1.md @@ -0,0 +1,5 @@ +Golang 在包级别提供封装。Go 没有任何 public、private 或 protected 关键字。控制可见性的唯一机制是使用大写和小写格式。 +--- +大写标识符是导出的。大写字母表示这是一个导出的标识符。小写标识符是未导出的。小写字母表示该标识符未导出,只能在同一个包内访问。 +--- +有五种标识符可以被导出或不导出。 diff --git a/locales/zh/214-Encapsulation/encap-2.md b/locales/zh/214-Encapsulation/encap-2.md new file mode 100644 index 0000000..c404e1a --- /dev/null +++ b/locales/zh/214-Encapsulation/encap-2.md @@ -0,0 +1,3 @@ +### 1. 导出结构体 +--- +这里,在 company 包中,结构体 PublicCompany 是导出的,结构体 privateCompany 是未导出的。 diff --git a/locales/zh/214-Encapsulation/encap-3.md b/locales/zh/214-Encapsulation/encap-3.md new file mode 100644 index 0000000..a316a7c --- /dev/null +++ b/locales/zh/214-Encapsulation/encap-3.md @@ -0,0 +1,3 @@ +### 2. 导出结构体方法 +--- +Person 结构体的方法 GetAge() 是导出的,getName 是未导出的。 diff --git a/locales/zh/214-Encapsulation/encap-4.md b/locales/zh/214-Encapsulation/encap-4.md new file mode 100644 index 0000000..b6443e6 --- /dev/null +++ b/locales/zh/214-Encapsulation/encap-4.md @@ -0,0 +1,3 @@ +### 3. 导出结构体字段 +--- +Student 结构体的字段 Name 是导出的。Student 结构体的字段 age 是未导出的。 diff --git a/locales/zh/214-Encapsulation/encap-5.md b/locales/zh/214-Encapsulation/encap-5.md new file mode 100644 index 0000000..469d274 --- /dev/null +++ b/locales/zh/214-Encapsulation/encap-5.md @@ -0,0 +1,3 @@ +### 4. 导出函数 +--- +函数 Sum 是导出的,函数 average 是未导出的。 diff --git a/locales/zh/214-Encapsulation/encap-6.md b/locales/zh/214-Encapsulation/encap-6.md new file mode 100644 index 0000000..3265fdb --- /dev/null +++ b/locales/zh/214-Encapsulation/encap-6.md @@ -0,0 +1,3 @@ +### 5. 导出变量 +--- +变量 PersonName 是导出的,变量 personAge 是未导出的。 diff --git a/locales/zh/215-Interfaces/interfaces-1.md b/locales/zh/215-Interfaces/interfaces-1.md new file mode 100644 index 0000000..9a55b5c --- /dev/null +++ b/locales/zh/215-Interfaces/interfaces-1.md @@ -0,0 +1,5 @@ +首先,我们定义一个非常简单的几何图形接口,包含两个最基本的接口方法——计算面积和计算周长,代码如下: +--- +接下来,我们定义两个结构体,一个矩形结构体和一个圆形结构体,代码如下: +--- +然后分别实现上面定义的接口方法,与矩形结构体相关的代码如下: diff --git a/locales/zh/215-Interfaces/interfaces-2.md b/locales/zh/215-Interfaces/interfaces-2.md new file mode 100644 index 0000000..b522faa --- /dev/null +++ b/locales/zh/215-Interfaces/interfaces-2.md @@ -0,0 +1 @@ +现在来看一个完整的代码示例,了解接口的实际用法如下: diff --git a/locales/zh/216-Interface-Satisfaction/interface-satisfy-1.md b/locales/zh/216-Interface-Satisfaction/interface-satisfy-1.md new file mode 100644 index 0000000..d8d6ae0 --- /dev/null +++ b/locales/zh/216-Interface-Satisfaction/interface-satisfy-1.md @@ -0,0 +1,7 @@ +在 XGo 中,接口不需要显式实现——即没有 implement 关键字。相反,接口是隐式满足的。 +--- +一个类型如果拥有接口所要求的所有方法,就满足了该接口。例如,*os.File 满足 io.Reader、io.Writer、io.Closer 和 io.ReadWriter。*bytes.Buffer 满足 io.Reader、io.Writer 和 io.ReadWriter,但不满足 io.Closer,因为它没有 Close 方法。 +--- +接口的可赋值规则非常简单:只有当表达式的类型满足接口时,才能将其赋值给接口。即使右侧本身就是接口,该规则同样适用。例如: +--- +因为 io.ReadWriter 和 io.ReadWriteCloser 包含了 io.Writer 的所有方法,任何满足 io.ReadWriter 或 io.ReadWriteCloser 的类型也必然满足 io.Writer。 diff --git a/locales/zh/216-Interface-Satisfaction/interface-satisfy-2.md b/locales/zh/216-Interface-Satisfaction/interface-satisfy-2.md new file mode 100644 index 0000000..8924853 --- /dev/null +++ b/locales/zh/216-Interface-Satisfaction/interface-satisfy-2.md @@ -0,0 +1 @@ +就像信封包裹并隐藏了它所装的信件一样,接口包裹并隐藏了它所持有的具体类型和值。即使具体类型有其他方法,也只能调用接口类型所暴露的方法。例如: diff --git a/locales/zh/216-Interface-Satisfaction/interface-satisfy-3.md b/locales/zh/216-Interface-Satisfaction/interface-satisfy-3.md new file mode 100644 index 0000000..9316b28 --- /dev/null +++ b/locales/zh/216-Interface-Satisfaction/interface-satisfy-3.md @@ -0,0 +1,9 @@ +一个具体类型可以满足许多不相关的接口。考虑一个组织或销售数字化文化产品(如音乐、电影和书籍)的程序。它可能定义以下一组具体类型: +--- + Album + Book + Movie + Magazine + Podcast + TVEpisode + Track diff --git a/locales/zh/216-Interface-Satisfaction/interface-satisfy-4.md b/locales/zh/216-Interface-Satisfaction/interface-satisfy-4.md new file mode 100644 index 0000000..b7838a3 --- /dev/null +++ b/locales/zh/216-Interface-Satisfaction/interface-satisfy-4.md @@ -0,0 +1 @@ +我们可以将每个感兴趣的抽象表示为一个接口。某些属性对所有产品都是通用的,例如标题、创建日期和创作者列表(作者或艺术家)。 diff --git a/locales/zh/216-Interface-Satisfaction/interface-satisfy-5.md b/locales/zh/216-Interface-Satisfaction/interface-satisfy-5.md new file mode 100644 index 0000000..9778dbe --- /dev/null +++ b/locales/zh/216-Interface-Satisfaction/interface-satisfy-5.md @@ -0,0 +1 @@ +其他属性仅限于特定类型的产品。印刷文字的属性仅与书籍和杂志相关,而只有电影和电视剧集才有屏幕分辨率。 diff --git a/locales/zh/216-Interface-Satisfaction/interface-satisfy-6.md b/locales/zh/216-Interface-Satisfaction/interface-satisfy-6.md new file mode 100644 index 0000000..291be53 --- /dev/null +++ b/locales/zh/216-Interface-Satisfaction/interface-satisfy-6.md @@ -0,0 +1,3 @@ +这些接口只是将相关具体类型组合在一起并表达它们共同特征的一种有用方式。我们以后可能会发现其他分组方式。例如,如果我们发现需要以相同方式处理 Audio 和 Video 项目,可以定义一个 Streamer 接口来表示它们的共同特征,而无需更改任何现有的类型声明。 +--- +基于共享行为的具体类型分组可以表示为接口类型。与基于类的语言不同——在那些语言中,类所满足的接口集合是显式声明的——在 Go 中,我们可以在需要时定义新的抽象或感兴趣的分组,而无需修改具体类型的声明。当具体类型来自不同作者编写的包时,这一点尤其有用。当然,具体类型之间确实需要存在底层的共性。 diff --git a/locales/zh/217-Interface-Values/interface-values-1.md b/locales/zh/217-Interface-Values/interface-values-1.md new file mode 100644 index 0000000..c1a0df4 --- /dev/null +++ b/locales/zh/217-Interface-Values/interface-values-1.md @@ -0,0 +1,7 @@ +接口类型的值和接口值是两个不同的概念。 +--- +XGo 是一种静态类型编程语言,类型是编译时的概念,因此类型不是值。类型描述符的值提供了每个类型的信息,例如其名称和方法。在接口值中,类型组件由相应的类型描述符表示。 +--- +在 XGo 中,接口变量与其他类型变量一样,始终初始化为定义良好的值。接口的零值将其类型和值组件都设置为 nil,例如: +--- +接口变量 w 具有零值。因此它的动态类型和动态值都是 nil。在这种情况下,if w == nil 的结果为 true。接口变量 r2 具有非零值。它的动态类型是 *bytes.Reader,动态值是 nil。因此 r2 == nil 的结果为 false。 diff --git a/locales/zh/217-Interface-Values/interface-values-2.md b/locales/zh/217-Interface-Values/interface-values-2.md new file mode 100644 index 0000000..81acb58 --- /dev/null +++ b/locales/zh/217-Interface-Values/interface-values-2.md @@ -0,0 +1,5 @@ +接口值可以使用 == 和 != 进行比较。两个接口值相等的条件是:两者都为 nil,或者它们的动态类型相同且动态值根据该类型的 == 的通常行为相等。因为接口值是可比较的,所以可以用作 map 的键或 switch 语句的操作数。 +--- +但是,如果比较的两个接口值具有相同的动态类型,但该类型不可比较(例如 slice),则比较会导致 panic。例如 +--- +在这方面,接口类型是特殊的。其他类型要么可以安全比较(如基本类型和指针),要么完全不可比较(如 slice、map 和函数),但在比较接口值或包含接口值的聚合类型时,我们必须注意潜在的 panic 风险。将接口用作 map 键或 switch 操作数时也存在类似的风险。只有在确定接口值包含可比较类型的动态值时,才进行接口值的比较。 diff --git a/locales/zh/217-Interface-Values/interface-values-3.md b/locales/zh/217-Interface-Values/interface-values-3.md new file mode 100644 index 0000000..4f1f599 --- /dev/null +++ b/locales/zh/217-Interface-Values/interface-values-3.md @@ -0,0 +1,3 @@ +在处理错误或调试时,报告接口值的动态类型通常很有帮助。为此,我们使用 fmt 包的 %T 动词: +--- +在内部,fmt 使用反射来获取接口动态类型的名称。 diff --git a/locales/zh/218-The-error-Interface/error-1.md b/locales/zh/218-The-error-Interface/error-1.md new file mode 100644 index 0000000..1b0a2c5 --- /dev/null +++ b/locales/zh/218-The-error-Interface/error-1.md @@ -0,0 +1 @@ +error 接口有一个返回错误消息的方法: diff --git a/locales/zh/218-The-error-Interface/error-2.md b/locales/zh/218-The-error-Interface/error-2.md new file mode 100644 index 0000000..453ce43 --- /dev/null +++ b/locales/zh/218-The-error-Interface/error-2.md @@ -0,0 +1,3 @@ +创建错误最简单的方式是调用 errors.New,它为给定的错误消息返回一个新的错误。整个 errors 包只有四行代码: +--- +errorString 的底层类型是结构体而不是字符串,这是为了保护其表示不被意外(或故意)修改。而指针类型 *errorString(而非 errorString 本身)满足 error 接口的原因是,这样每次调用 New 都会分配一个不同的错误实例,与其他任何错误都不相等。 diff --git a/locales/zh/218-The-error-Interface/error-3.md b/locales/zh/218-The-error-Interface/error-3.md new file mode 100644 index 0000000..2cc4f9e --- /dev/null +++ b/locales/zh/218-The-error-Interface/error-3.md @@ -0,0 +1 @@ +我们不希望像 io.EOF 这样的特定错误与一个恰好具有相同消息的错误比较相等。 diff --git a/locales/zh/218-The-error-Interface/error-4.md b/locales/zh/218-The-error-Interface/error-4.md new file mode 100644 index 0000000..d972c8f --- /dev/null +++ b/locales/zh/218-The-error-Interface/error-4.md @@ -0,0 +1 @@ +直接调用 errors.New 的情况相对较少,因为有一个方便的包装函数 fmt.Errorf,它还能进行字符串格式化。 diff --git a/locales/zh/219-Type-Assertions/type-assert-1.md b/locales/zh/219-Type-Assertions/type-assert-1.md new file mode 100644 index 0000000..4d3f82b --- /dev/null +++ b/locales/zh/219-Type-Assertions/type-assert-1.md @@ -0,0 +1,3 @@ +类型断言是应用于接口值的操作。语法上,它看起来像 x.(T),其中 x 是接口类型的表达式,T 是一个类型,称为"断言"类型。类型断言检查其操作数的动态类型是否匹配断言的类型。 +--- +有两种可能。首先,如果断言类型 T 是具体类型,则类型断言检查 x 的动态类型是否与 T 相同。如果检查成功,类型断言的结果是 x 的动态值,其类型当然是 T。换句话说,对具体类型的类型断言从其操作数中提取具体值。如果检查失败,则操作会 panic。例如: diff --git a/locales/zh/219-Type-Assertions/type-assert-2.md b/locales/zh/219-Type-Assertions/type-assert-2.md new file mode 100644 index 0000000..70e03de --- /dev/null +++ b/locales/zh/219-Type-Assertions/type-assert-2.md @@ -0,0 +1 @@ +其次,如果断言类型 T 是接口类型,则类型断言检查 x 的动态类型是否满足 T。如果检查成功,动态值不会被提取;结果仍然是一个具有相同类型和值组件的接口值,但结果具有接口类型 T。换句话说,对接口类型的类型断言改变了表达式的类型,使得一组不同的(通常更大的)方法可以被访问,但它保留了接口值内部的动态类型和值组件。 diff --git a/locales/zh/219-Type-Assertions/type-assert-3.md b/locales/zh/219-Type-Assertions/type-assert-3.md new file mode 100644 index 0000000..e4bba1c --- /dev/null +++ b/locales/zh/219-Type-Assertions/type-assert-3.md @@ -0,0 +1 @@ +无论断言的是什么类型,如果操作数是 nil 接口值,类型断言都会失败。对限制更少的接口类型(方法更少的接口类型)进行类型断言很少需要,因为它的行为与赋值类似,除了在 nil 的情况下。 diff --git a/locales/zh/219-Type-Assertions/type-assert-4.md b/locales/zh/219-Type-Assertions/type-assert-4.md new file mode 100644 index 0000000..b817927 --- /dev/null +++ b/locales/zh/219-Type-Assertions/type-assert-4.md @@ -0,0 +1,3 @@ +通常我们不确定接口值的动态类型,我们想测试它是否是某个特定类型。如果类型断言出现在期望两个结果的赋值中,例如以下声明,操作在失败时不会 panic,而是返回一个额外的第二个结果——一个表示成功与否的布尔值: +--- +第二个结果通常赋值给名为 ok 的变量。如果操作失败,ok 为 false,第一个结果等于断言类型的零值,在本示例中是 nil *bytes.Buffer。 diff --git a/locales/zh/220-Type-Switches/type-switch.md b/locales/zh/220-Type-Switches/type-switch.md new file mode 100644 index 0000000..dd7d730 --- /dev/null +++ b/locales/zh/220-Type-Switches/type-switch.md @@ -0,0 +1 @@ +类型 `switch` 比较的是类型而不是值。你可以使用它来判断接口值的类型。在这个示例中,变量 `t` 将具有与其子句对应的类型。 diff --git a/main.go b/main.go index 263c888..64de7d4 100644 --- a/main.go +++ b/main.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/json" "flag" "fmt" "log" @@ -12,31 +13,162 @@ import ( "strconv" "strings" "sync" - "sync/atomic" "text/template" - "unsafe" gohtml "html" + htmltemplate "html/template" "github.com/goplus/tutorial/internal" "github.com/russross/blackfriday/v2" ) const ( - chNumLen = 3 + chNumLen = 3 + defaultLang = "en" ) +// supportedLangs defines the non-default languages supported by the site. +var supportedLangs = map[string]bool{"zh": true} + +// translations holds locale strings keyed by language then by key. +var translations map[string]map[string]string + +// titleTranslations holds title translations keyed by language then by original title. +var titleTranslations map[string]map[string]string + +// loadTranslations reads all JSON files from the locales/ directory. +func loadTranslations(dir string) { + translations = make(map[string]map[string]string) + titleTranslations = make(map[string]map[string]string) + fis, err := os.ReadDir(dir) + if err != nil { + log.Printf("Warning: cannot read locales directory %s: %v", dir, err) + return + } + for _, fi := range fis { + if fi.IsDir() || !strings.HasSuffix(fi.Name(), ".json") { + continue + } + lang := strings.TrimSuffix(fi.Name(), ".json") + data, err := os.ReadFile(filepath.Join(dir, fi.Name())) + if err != nil { + log.Printf("Warning: cannot read locale file %s: %v", fi.Name(), err) + continue + } + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + log.Printf("Warning: cannot parse locale file %s: %v", fi.Name(), err) + continue + } + m := make(map[string]string) + for k, v := range raw { + if k == "titles" { + titles := make(map[string]string) + if err := json.Unmarshal(v, &titles); err == nil { + titleTranslations[lang] = titles + } + continue + } + var s string + if err := json.Unmarshal(v, &s); err == nil { + m[k] = s + } + } + translations[lang] = m + } +} + +// lookup searches a nested translation map for the given language and key, +// falling back to the default language. +func lookup(dict map[string]map[string]string, lang, key string) (string, bool) { + if m, ok := dict[lang]; ok { + if v, ok := m[key]; ok { + return v, true + } + } + if lang != defaultLang { + if m, ok := dict[defaultLang]; ok { + if v, ok := m[key]; ok { + return v, true + } + } + } + return "", false +} + +// t returns the translated string for the given language and key. +func t(lang, key string) htmltemplate.HTML { + if v, ok := lookup(translations, lang, key); ok { + return htmltemplate.HTML(v) + } + return htmltemplate.HTML(key) +} + +// translateTitle returns the localized title for the given language. +func translateTitle(lang, title string) string { + if v, ok := lookup(titleTranslations, lang, title); ok { + return v + } + return title +} + +// extractLang parses the URL path to extract a language prefix. +// Returns (defaultLang, originalPath) for English, or (lang, cleanPath) for other languages. +func extractLang(urlPath string) (lang, cleanPath string) { + trimmed := strings.TrimPrefix(urlPath, "/") + parts := strings.SplitN(trimmed, "/", 2) + if len(parts) >= 1 && supportedLangs[parts[0]] { + lang = parts[0] + if len(parts) == 2 { + cleanPath = "/" + parts[1] + } else { + cleanPath = "/" + } + return + } + return defaultLang, urlPath +} + +// langPrefix returns the URL prefix for a language, e.g. "/zh" or "" for English. +func langPrefix(lang string) string { + if lang == defaultLang { + return "" + } + return "/" + lang +} + var ( headerTempl string footerTempl string + indexTmpl *template.Template exampleTmpl *template.Template ) +// templateFuncMap provides shared template functions including translation. +var templateFuncMap = template.FuncMap{ + "t": func(lang, key string) htmltemplate.HTML { + return t(lang, key) + }, + "langPrefix": langPrefix, + "translateTitle": translateTitle, +} + func init() { + loadTranslations("locales") + headerTempl = mustReadFile("templates/header.tmpl") footerTempl = mustReadFile("templates/footer.tmpl") - exampleTmpl = template.New("example") - _, err := exampleTmpl.Parse(headerTempl) + + indexTmpl = template.New("index").Funcs(templateFuncMap) + _, err := indexTmpl.Parse(headerTempl) + check(err) + _, err = indexTmpl.Parse(footerTempl) + check(err) + _, err = indexTmpl.Parse(mustReadFile("templates/index.tmpl")) + check(err) + + exampleTmpl = template.New("example").Funcs(templateFuncMap) + _, err = exampleTmpl.Parse(headerTempl) check(err) _, err = exampleTmpl.Parse(footerTempl) check(err) @@ -64,7 +196,7 @@ type exampleIndex struct { Title string Prev *exampleIndex Next *exampleIndex - cache *example + cache sync.Map // lang -> *example } type chapter struct { @@ -74,6 +206,7 @@ type chapter struct { var ( exampleIndexes map[string]*exampleIndex + tutorialNames []string watcher *internal.Watcher ) @@ -96,23 +229,21 @@ func listTutorial(dir string) (names []string, err error) { return } -func renderIndex(tutorial []string) []byte { - indexTmpl := template.New("index") - _, err := indexTmpl.Parse(headerTempl) - check(err) - _, err = indexTmpl.Parse(footerTempl) - check(err) - _, err = indexTmpl.Parse(mustReadFile("templates/index.tmpl")) - check(err) +// indexData wraps the data passed to the index template. +type indexData struct { + Lang string + Chapters []*chapter +} - var buf bytes.Buffer +// buildExampleIndexes builds the global exampleIndexes and chapter structure from tutorial names. +func buildExampleIndexes(tutorial []string) []*chapter { var indexes = make(map[string]*exampleIndex, len(tutorial)) var chs []*chapter var ch *chapter var prev *exampleIndex for _, name := range tutorial { title := name[chNumLen+1:] - titleEsc := strings.ReplaceAll(title, "-", " ") + titleEsc := gohtml.UnescapeString(strings.ReplaceAll(title, "-", " ")) if strings.HasSuffix(name[:chNumLen], "00") { ch = &chapter{Title: titleEsc} chs = append(chs, ch) @@ -136,7 +267,17 @@ func renderIndex(tutorial []string) []byte { prev = idx } exampleIndexes = indexes - err = indexTmpl.Execute(&buf, chs) + return chs +} + +// renderIndex renders the index page for the given language using pre-built chapters. +func renderIndex(chapters []*chapter, lang string) []byte { + data := &indexData{ + Lang: lang, + Chapters: chapters, + } + var buf bytes.Buffer + err := indexTmpl.Execute(&buf, data) check(err) return buf.Bytes() } @@ -251,6 +392,30 @@ func parseAndRenderSegs(sourcePath string) []*Seg { return segs } +// overrideDocsFromMD replaces the document segments in segs with content from a Markdown file. +// The Markdown file uses "---" separators to delimit sections that correspond to doc segments. +func overrideDocsFromMD(segs []*Seg, mdContent, mdPath string) { + sections := strings.Split(mdContent, "\n---\n") + docCount := 0 + for _, seg := range segs { + if seg.Docs != nil { + docCount++ + } + } + if len(sections) != docCount { + log.Printf("Warning: %s has %d sections but .gop has %d doc segments", mdPath, len(sections), docCount) + } + docIdx := 0 + for _, seg := range segs { + if seg.Docs != nil && docIdx < len(sections) { + text := strings.TrimSpace(sections[docIdx]) + seg.Docs = []string{text} + seg.DocsRendered = string(blackfriday.Run([]byte(text))) + docIdx++ + } + } +} + // ----------------------------------------------------------------------------- type exampleFile struct { @@ -264,6 +429,13 @@ type exampleFile struct { TailDoc []*Seg } +// exampleData wraps the example with language info for template rendering. +type exampleData struct { + *exampleIndex + Files []*exampleFile + Lang string +} + type example struct { *exampleIndex Files []*exampleFile @@ -297,42 +469,68 @@ func langOf(fname string) string { return fname[i+1:] } -func parseExample(dir string, idx *exampleIndex) *example { +// replaceExt replaces the file extension with a new one. +func replaceExt(fname, newExt string) string { + i := strings.LastIndex(fname, ".") + if i < 0 { + return fname + newExt + } + return fname[:i] + newExt +} + +func parseExample(dir string, idx *exampleIndex, lang string) *example { fis, err := os.ReadDir(dir) check(err) - example := &example{exampleIndex: idx} + ex := &example{exampleIndex: idx} for _, fi := range fis { fname := fi.Name() - lang := langOf(fname) - if lang != "xgo" && lang != "gop" { // only support XGo examples + fileLang := langOf(fname) + if fileLang != "xgo" && fileLang != "gop" { // only support XGo examples continue } sourcePath := filepath.Join(dir, fname) sourceSegs := parseAndRenderSegs(sourcePath) - if len(sourceSegs) != 0 { // ignore file with no segs - headDoc, code, tailDoc := classifySegs(sourceSegs) - file := &exampleFile{lang, headDoc, code, tailDoc} - example.Files = append(example.Files, file) + if len(sourceSegs) == 0 { // ignore file with no segs + continue + } + + // Try to load a language-specific .md override. + // sourceSegs are freshly parsed so we can mutate them directly. + if lang != defaultLang { + mdPath := filepath.Join("locales", lang, idx.Name, replaceExt(fname, ".md")) + if mdContent, err := os.ReadFile(mdPath); err == nil { + overrideDocsFromMD(sourceSegs, string(mdContent), mdPath) + } } + + headDoc, code, tailDoc := classifySegs(sourceSegs) + file := &exampleFile{fileLang, headDoc, code, tailDoc} + ex.Files = append(ex.Files, file) } - return example + return ex } -func renderExample(e *example) []byte { +func renderExample(e *example, lang string) []byte { + data := &exampleData{ + exampleIndex: e.exampleIndex, + Files: e.Files, + Lang: lang, + } var buf bytes.Buffer - err := exampleTmpl.Execute(&buf, e) + err := exampleTmpl.Execute(&buf, data) check(err) return buf.Bytes() } -func handleExample(w http.ResponseWriter, req *http.Request, root, path string) { - if idx, ok := exampleIndexes[path]; ok { - cache := atomic.LoadPointer((*unsafe.Pointer)(unsafe.Pointer(&idx.cache))) - if cache == nil { - cache = unsafe.Pointer(parseExample(filepath.Join(root, idx.Name), idx)) - atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&idx.cache)), cache) +func handleExample(w http.ResponseWriter, req *http.Request, root, urlPath, lang string) { + if idx, ok := exampleIndexes[urlPath]; ok { + cached, ok := idx.cache.Load(lang) + if !ok { + ex := parseExample(filepath.Join(root, idx.Name), idx, lang) + idx.cache.Store(lang, ex) + cached = ex } - data := renderExample((*example)(cache)) + data := renderExample(cached.(*example), lang) w.Write(data) return } @@ -342,7 +540,8 @@ func handleExample(w http.ResponseWriter, req *http.Request, root, path string) // ----------------------------------------------------------------------------- func handle(root string) func(w http.ResponseWriter, req *http.Request) { - var text []byte + var indexCache sync.Map // lang -> []byte + var chapters []*chapter var wg sync.WaitGroup wg.Add(1) go func() { @@ -350,13 +549,17 @@ func handle(root string) func(w http.ResponseWriter, req *http.Request) { if err != nil { log.Panicln(err) } - text = renderIndex(names) + tutorialNames = names + chapters = buildExampleIndexes(names) + enIndex := renderIndex(chapters, defaultLang) + indexCache.Store(defaultLang, enIndex) wg.Done() watcher, err = internal.NewWatcher(func(string) { for _, v := range exampleIndexes { - atomic.StorePointer((*unsafe.Pointer)(unsafe.Pointer(&v.cache)), nil) + v.cache = sync.Map{} } + indexCache = sync.Map{} }) if err != nil { log.Panicln(err) @@ -371,16 +574,25 @@ func handle(root string) func(w http.ResponseWriter, req *http.Request) { if !path.IsAbs(urlPath) { urlPath = "/404.html" } - if urlPath == "/" { + + lang, cleanPath := extractLang(urlPath) + + if cleanPath == "/" { wg.Wait() - w.Write(text) + cached, ok := indexCache.Load(lang) + if !ok { + text := renderIndex(chapters, lang) + indexCache.Store(lang, text) + cached = text + } + w.Write(cached.([]byte)) return } - if path.Ext(urlPath) != "" { - http.ServeFile(w, req, "./public"+urlPath) + if path.Ext(cleanPath) != "" { + http.ServeFile(w, req, "./public"+cleanPath) return } - handleExample(w, req, root, urlPath) + handleExample(w, req, root, cleanPath, lang) } } diff --git a/public/site.css b/public/site.css index 163a778..88393ec 100644 --- a/public/site.css +++ b/public/site.css @@ -79,19 +79,89 @@ body { } } -/* breadcrumb */ -.breadcrumb { +/* top bar: breadcrumb + language switcher */ +.top-bar { display: flex; - flex-direction: row; + justify-content: space-between; + align-items: center; margin-top: 36px; - font-size: 16px; - line-height: 1.5; } @media screen and (max-width: 640px) { - .breadcrumb { + .top-bar { margin-top: 0; } } + +/* language switcher dropdown */ +.lang-switcher { + position: relative; +} +.lang-switcher-btn { + display: flex; + align-items: center; + gap: 4px; + padding: 0; + border: none; + background: none; + color: #999; + font-size: 16px; + line-height: 1.5; + cursor: pointer; + transition: color .3s; + font-family: inherit; +} +.lang-switcher-btn:hover { + color: #333; +} +.lang-icon { + flex-shrink: 0; +} +.lang-arrow { + flex-shrink: 0; + transition: transform .2s; +} +.lang-switcher.open .lang-arrow { + transform: rotate(180deg); +} +.lang-menu { + display: none; + position: absolute; + top: calc(100% + 6px); + right: 0; + min-width: 100px; + padding: 4px 0; + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + z-index: 100; +} +.lang-switcher.open .lang-menu { + display: block; +} +.lang-menu-item { + display: block; + padding: 6px 14px; + color: #333; + font-size: 14px; + text-decoration: none; + transition: background-color .15s; +} +.lang-menu-item:hover { + background-color: #f5f5f5; +} +.lang-menu-item.active { + color: #2C84FF; + font-weight: 500; +} + +/* breadcrumb */ +.breadcrumb { + display: flex; + flex-direction: row; + font-size: 16px; + line-height: 1.5; +} .breadcrumb-link-item, .breadcrumb-sep { color: #999999; } diff --git a/templates/example.tmpl b/templates/example.tmpl index 8c3e15f..2ae087b 100644 --- a/templates/example.tmpl +++ b/templates/example.tmpl @@ -1,8 +1,8 @@ - + - XGo by Tutorials: {{.Title}} + {{t .Lang "site_title"}}: {{translateTitle .Lang .Title}} @@ -11,25 +11,38 @@ {{ template "header" }}
- +
+ +
+ +
+ English + 中文 +
+
+
-

{{.Title}}

+

{{translateTitle .Lang .Title}}

{{if .Files}}{{else}} -

No content yet, you can help us build it here.

+

{{t .Lang "no_content_before"}}{{t .Lang "no_content_link"}}{{t .Lang "no_content_after"}}

{{end}} {{range .Files}} {{if .HeadDoc}} @@ -52,7 +65,7 @@ {{end}} {{if .Next}}

- Next example: {{.Next.Title}} + {{t .Lang "next_example"}} {{translateTitle .Lang .Next.Title}}

{{end}}
@@ -61,6 +74,13 @@ diff --git a/templates/index.tmpl b/templates/index.tmpl index e114a2a..6ac9422 100644 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -1,8 +1,8 @@ - + - XGo by Tutorials + {{t .Lang "site_title"}} @@ -11,31 +11,50 @@ {{ template "header" }}
- +
+ +
+ +
+ English + 中文 +
+
+
-

Tutorials

+

{{t .Lang "index_heading"}}

- XGo is an open source programming language aimed to enable everyone to become a builder of the world. + {{t .Lang "index_desc_1"}}

- XGo by Tutorials is a hands-on introduction - to XGo using annotated example programs. Check out - the first example or - browse the full list below. + {{t .Lang "index_desc_2"}}

- {{range .}} - {{if .Title}}{{end}} + {{range .Chapters}} + {{if .Title}}{{end}} {{end}}
{{ template "footer" }} + From 6a139612c87d2b001085e3520cdd6e1c9f5e8bab Mon Sep 17 00:00:00 2001 From: Miclle Zheng Date: Sat, 18 Apr 2026 21:36:07 +0800 Subject: [PATCH 2/5] fix(i18n): localize language switch label Add localized aria-label text for the language switcher and keep the page templates aligned with the existing translation flow. --- locales/en.json | 3 ++- locales/zh.json | 1 + templates/example.tmpl | 2 +- templates/index.tmpl | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/locales/en.json b/locales/en.json index 4ae56f4..31316ff 100644 --- a/locales/en.json +++ b/locales/en.json @@ -7,5 +7,6 @@ "no_content_before": "No content yet, you can help us build it ", "no_content_link": "here", "no_content_after": ".", - "next_example": "Next example:" + "next_example": "Next example:", + "lang_switcher_label": "Switch language" } diff --git a/locales/zh.json b/locales/zh.json index 6a91726..9287390 100644 --- a/locales/zh.json +++ b/locales/zh.json @@ -8,6 +8,7 @@ "no_content_link": "这里", "no_content_after": "帮助我们完善。", "next_example": "下一个示例:", + "lang_switcher_label": "切换语言", "titles": { "Sequential programming": "Sequential Programming 顺序式编程", "Structured programming": "Structured Programming 结构化编程", diff --git a/templates/example.tmpl b/templates/example.tmpl index 2ae087b..e7595b7 100644 --- a/templates/example.tmpl +++ b/templates/example.tmpl @@ -28,7 +28,7 @@
-
- English - 中文 + {{range allLangs}} + {{.Name}} + {{end}}
diff --git a/templates/index.tmpl b/templates/index.tmpl index 25c6b10..a5aab1e 100644 --- a/templates/index.tmpl +++ b/templates/index.tmpl @@ -19,12 +19,13 @@
- English - 中文 + {{range allLangs}} + {{.Name}} + {{end}}
From 5eaeb718b822f56ddd41b7a3d4e6f3a8cd009a54 Mon Sep 17 00:00:00 2001 From: Miclle Zheng Date: Sat, 18 Apr 2026 23:03:23 +0800 Subject: [PATCH 5/5] fix: address PR review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - gen-translation.go: validate lang argument against path traversal - gen-translation.go: validate full 3-digit prefix instead of first char - gen-translation.go: distinguish os.Stat errors from file-not-found - gen-translation.go: hoist os.MkdirAll outside inner file loop - gen-translation.go: fix --- separator with blank lines for CommonMark - gen-translation.go: fix heading prefix to avoid ## # malformed headings - Fix "Go" → "XGo" in slices-10.md for zh/ja/ko - Add empty titles object to en.json for structural consistency --- gen-translation.go | 28 ++++++++++++++++++++-------- locales/en.json | 3 ++- locales/ja/117-Slices/slices-10.md | 2 +- locales/ko/117-Slices/slices-10.md | 2 +- locales/zh/117-Slices/slices-10.md | 2 +- 5 files changed, 25 insertions(+), 12 deletions(-) diff --git a/gen-translation.go b/gen-translation.go index f262760..16624e8 100644 --- a/gen-translation.go +++ b/gen-translation.go @@ -21,6 +21,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strconv" "strings" ) @@ -33,6 +34,10 @@ func main() { os.Exit(1) } lang := os.Args[1] + if !regexp.MustCompile(`^[a-z]{2,10}$`).MatchString(lang) { + fmt.Fprintln(os.Stderr, "Error: lang must be a simple language code (e.g. zh, ja, ko)") + os.Exit(1) + } var dirs []string if len(os.Args) > 2 { @@ -48,7 +53,7 @@ func main() { if fi.IsDir() { name := fi.Name() if len(name) > (chNumLen+1) && name[chNumLen] == '-' { - if _, e := strconv.Atoi(name[:1]); e == nil { + if _, e := strconv.Atoi(name[:chNumLen]); e == nil { // Skip chapter headings (e.g. 100-, 200-) if !strings.HasSuffix(name[:chNumLen], "00") { dirs = append(dirs, name) @@ -67,6 +72,8 @@ func main() { fmt.Fprintf(os.Stderr, "Warning: cannot read %s: %v\n", dir, err) continue } + langDir := filepath.Join("locales", lang, dir) + dirCreated := false for _, fi := range fis { fname := fi.Name() ext := filepath.Ext(fname) @@ -81,6 +88,9 @@ func main() { if _, err := os.Stat(mdPath); err == nil { skipped++ continue + } else if !os.IsNotExist(err) { + fmt.Fprintf(os.Stderr, "Warning: cannot stat %s: %v\n", mdPath, err) + continue } // Parse .gop to extract doc segments @@ -95,15 +105,17 @@ func main() { continue } - // Create lang directory - langDir := filepath.Join("locales", lang, dir) - if err := os.MkdirAll(langDir, 0755); err != nil { - fmt.Fprintf(os.Stderr, "Error creating %s: %v\n", langDir, err) - continue + // Create lang directory once per tutorial dir + if !dirCreated { + if err := os.MkdirAll(langDir, 0755); err != nil { + fmt.Fprintf(os.Stderr, "Error creating %s: %v\n", langDir, err) + break + } + dirCreated = true } // Write skeleton .md - skeleton := strings.Join(docs, "\n---\n") + skeleton := strings.Join(docs, "\n\n---\n\n") if err := os.WriteFile(mdPath, []byte(skeleton+"\n"), 0644); err != nil { fmt.Fprintf(os.Stderr, "Error writing %s: %v\n", mdPath, err) continue @@ -133,7 +145,7 @@ func extractDocSegments(content string) []string { docText = strings.TrimPrefix(trimmed[2:], " ") } else if strings.HasPrefix(trimmed, "#") && !strings.HasPrefix(trimmed, "#!") { isDoc = true - docText = "##" + trimmed + docText = "## " + strings.TrimPrefix(strings.TrimPrefix(trimmed, "#"), " ") } if isDoc { diff --git a/locales/en.json b/locales/en.json index 31316ff..2d1f28e 100644 --- a/locales/en.json +++ b/locales/en.json @@ -8,5 +8,6 @@ "no_content_link": "here", "no_content_after": ".", "next_example": "Next example:", - "lang_switcher_label": "Switch language" + "lang_switcher_label": "Switch language", + "titles": {} } diff --git a/locales/ja/117-Slices/slices-10.md b/locales/ja/117-Slices/slices-10.md index 602ed16..becfd67 100644 --- a/locales/ja/117-Slices/slices-10.md +++ b/locales/ja/117-Slices/slices-10.md @@ -1,5 +1,5 @@ ### スライスへの要素追加 -スライスに新しい要素を追加するのは一般的な操作なので、Go は組み込みの append 関数を提供しています。組み込みパッケージのドキュメントに append の使い方が記載されています。 +スライスに新しい要素を追加するのは一般的な操作なので、XGo は組み込みの append 関数を提供しています。組み込みパッケージのドキュメントに append の使い方が記載されています。 func append(s []T, vs ...T) []T diff --git a/locales/ko/117-Slices/slices-10.md b/locales/ko/117-Slices/slices-10.md index 88d4d3e..e6473f4 100644 --- a/locales/ko/117-Slices/slices-10.md +++ b/locales/ko/117-Slices/slices-10.md @@ -1,5 +1,5 @@ ### 슬라이스에 요소 추가하기 -슬라이스에 새 요소를 추가하는 것은 매우 일반적인 작업이므로, Go는 내장 append 함수를 제공합니다. 내장 패키지 문서에서 append의 사용법을 설명합니다. +슬라이스에 새 요소를 추가하는 것은 매우 일반적인 작업이므로, XGo는 내장 append 함수를 제공합니다. 내장 패키지 문서에서 append의 사용법을 설명합니다. func append(s []T, vs ...T) []T diff --git a/locales/zh/117-Slices/slices-10.md b/locales/zh/117-Slices/slices-10.md index 24b743c..cfb1317 100644 --- a/locales/zh/117-Slices/slices-10.md +++ b/locales/zh/117-Slices/slices-10.md @@ -1,5 +1,5 @@ ### 向切片追加元素 -向切片追加新元素是很常见的操作,因此 Go 提供了内置的 append 函数。内置包的文档中描述了 append 的用法。 +向切片追加新元素是很常见的操作,因此 XGo 提供了内置的 append 函数。内置包的文档中描述了 append 的用法。 func append(s []T, vs ...T) []T