הבנה של נראה עלי בגו

הקדמה

בעת יצירת חבילה ב-Go, המטרה הסופית היא בדרך כלל להפוך את החבילה לנגישה למפתחים אחרים לשימוש, בחבילות מסדר גבוה יותר או בתוכניות מלאות. על ידי ייבוא החבילה, קטע הקוד שלך יכול לשמש כבלוק בניין לכלים אחרים, מורכבים יותר. עם זאת, רק חלק מהחבילות זמינות לייבוא. זה נקבע על ידי הנראות של החבילה.

נראות בהקשר זה משמעו את מרחב הקובץ ממנו ניתן להתייחס לחבילה או למבנה אחר. לדוגמה, אם אנו מגדירים משתנה בתוך פונקציה, הנראות (היקף) של משתנה זה היא רק בתוך הפונקציה שבה הוגדר. באופן דומה, אם אתה מגדיר משתנה בחבילה, תוכל להפוך אותו לנראה רק לחבילה זו, או לאפשר לו להיות גלוי גם מחוץ לחבילה.

שליטה מדוקדקת ברמת הגישה לחבילות חשובה בעת כתיבת קוד יעיל, במיוחד כאשר מתחשבים בשינויים עתידיים שאתה עשוי לרצות לבצע בחבילה שלך. אם אתה צריך לתקן באג, לשפר ביצועים או לשנות פונקציונליות, תרצה לבצע את השינוי באופן שלא יפרוץ את הקוד של מי שמשתמש בחבילה שלך. דרך אחת למזער שינויים שמפריעים היא לאפשר גישה רק לחלקים מהחבילה שנחוצים לשימוש הנכון שלה. על ידי הגבלת הגישה, אתה יכול לבצע שינויים פנימיים בחבילה שלך עם פחות סיכוי להשפיע על אופן השימוש של מפתחים אחרים בחבילה שלך.

במאמר זה, תלמד כיצד לשלוט ברמת הגישה לחבילות, כמו גם כיצד להגן על חלקים מהקוד שצריכים לשמש רק בתוך החבילה שלך. כדי לעשות זאת, ניצור יומן בסיסי ליומן וניפוי שגיאות, תוך שימוש בחבילות עם רמות גישה שונות לפריטים.

דרישות מוקדמות

כדי לעקוב אחרי הדוגמאות במאמר זה, תצטרך:

.
├── bin 
│ 
└── src
    └── github.com
        └── gopherguides

פריטים מיוצאים ולא מיוצאים

בניגוד לשפות תכנות אחרות כמו Java ו-Python שמשתמשות במגבלות גישה כמו public, private, או protected לציון היקף, Go קובעת אם פריט הוא מיוצא ו-לא מיוצא דרך הצהרתו. ייצוא פריט במקרה זה הופך אותו לנראה מחוץ לחבילה הנוכחית. אם הוא לא מיוצא, הוא נראה וניתן לשימוש רק מתוך החבילה שבה הוגדר.

הנראות החיצונית נשלטת על ידי האות הראשונה הגדולה של הפריט שהוכרז. כל הצהרות, כמו סוגים, משתנים, קבועים, פונקציות וכו', שמתחילות באות גדולה נראות מחוץ לחבילה הנוכחית.

בואו נבחן את הקוד הבא, תוך שימוש קשב לאותיות הגדולות:

greet.go
package greet

import "fmt"

var Greeting string

func Hello(name string) string {
	return fmt.Sprintf(Greeting, name)
}

קוד זה קובע שהוא נמצא בחבילת greet. לאחר מכן, הוא קובע שני סמלים, משתנה בשם Greeting ופונקציה בשם Hello. מכיוון ששניהם מתחילים באות גדולה, הם שניהם יוצאים וזמינים לכל תוכנית חיצונית. כפי שצוין קודם, יצירת חבילה שמגבילה גישה תאפשר עיצוב API טוב יותר ותקל על עדכון החבילה שלך באופן פנימי מבלי לשבור קוד של אחרים שתלוי בחבילה שלך.

הגדרת נראות חבילה

כדי להביט יותר מקרוב כיצד נראות החבילה עובדת בתוכנית, בואו ניצור חבילת logging, תוך התחשבות במה שאנו רוצים להפוך לנראה מחוץ לחבילה שלנו ובמה שלא נפיץ. חבילת היומונג זו תהיה אחראית ליומונג כל הודעות התוכנית שלנו לקונסולה. היא גם תבדוק באיזה רמה אנו מתעדים. רמה מתארת את סוג היומן, והיא תהיה אחת משלוש מצבים: info, warning, או error.

ראשית, בתוך תיקיית src שלך, בואו ניצור תיקייה בשם logging כדי להכניס את קבצי היומונג שלנו לתוכה:

  1. mkdir logging

זז לתוך התיקייה הזו הבאה:

  1. cd logging

לאחר מכן, באמצעות עורך כמו nano, צור קובץ בשם logging.go:

  1. nano logging.go

שים את הקוד הבא בקובץ logging.go שיצרנו זה עתה:

logging/logging.go
package logging

import (
	"fmt"
	"time"
)

var debug bool

func Debug(b bool) {
	debug = b
}

func Log(statement string) {
	if !debug {
		return
	}

	fmt.Printf("%s %s\n", time.Now().Format(time.RFC3339), statement)
}

השורה הראשונה של הקוד הזה הכריזה על חבילה בשם logging. בחבילה זו, יש שתי פונקציות מיוצאות: Debug ו-Log. ניתן לקרוא לפונקציות אלו על ידי כל חבילה אחרת שייבאה את החבילה logging. יש גם משתנה פרטי בשם debug. משתנה זה נגיש רק מתוך החבילה logging. חשוב לציין שבעוד שהפונקציה Debug והמשתנה debug שניהם בעלי אותה הכתיבה, הפונקציה היא באות גדולה והמשתנה אינו. זה הופך אותם להכרזות נפרדות עם תחומים שונים.

שמור וצא מהקובץ.

כדי להשתמש בחבילה זו בשטחים אחרים של הקוד שלנו, נוכל לייבא אותה לתוך חבילה חדשה. ניצור חבילה חדשה זו, אך נצטרך תחילה ספרייה חדשה לאחסון קבצי המקור האלה בה.

בואו נזיז את עצמנו מחוץ לספרייה logging, ניצור ספרייה חדשה בשם cmd, ונעבור לספרייה החדשה זו:

  1. cd ..
  2. mkdir cmd
  3. cd cmd

צור קובץ בשם main.go בספריית cmd שיצרנו זה עתה:

  1. nano main.go

עכשיו נוכל להוסיף את הקוד הבא:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

	logging.Log("This is a debug statement...")
}

כעת יש לנו את כל התוכנית שלנו כתובה. עם זאת, לפני שנוכל להריץ את התוכנית הזו, נצטרך גם ליצור כמה קבצי תצורה כדי שהקוד שלנו יעבוד כראוי. Go משתמשת ב-Go Modules כדי להגדיר תלויות חבילות לייבוא משאבים. מודולי Go הם קבצי תצורה שממוקמים במדריך החבילה שלך שמודיעים למהדר מאיפה לייבא חבילות. למרות שלמידה על מודולים חורגת מתחום מאמר זה, אנו יכולים לכתוב רק כמה שורות של תצורה כדי לגרום לדוגמה זו לעבוד באופן מקומי.

פתח את הקובץ go.mod במדריך cmd:

  1. nano go.mod

והכנס את התוכן הבא לקובץ:

go.mod
module github.com/gopherguides/cmd

replace github.com/gopherguides/logging => ../logging

השורה הראשונה בקובץ זה אומרת למהדר של החבילה cmd יש נתיב קובץ של github.com/gopherguides/cmd. השורה השנייה אומרת למהדר שהחבילה github.com/gopherguides/logging ניתן למצוא בדיסק מקומי במדריך ../logging.

נצטרך גם קובץ go.mod עבור החבילה שלנו logging. בואו נחזור למדריך logging וניצור קובץ go.mod:

  1. cd ../logging
  2. nano go.mod

הוסף את התוכן הבא לקובץ:

go.mod
module github.com/gopherguides/logging

זה אומר למהדר שהחבילה logging שיצרנו היא למעשה החבילה github.com/gopherguides/logging. זה מאפשר לייבא את החבילה בחבילה שלנו main עם השורה הבאה שכתבנו קודם:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

	logging.Log("This is a debug statement...")
}

כעת עליך להיות עם מבנה המדריכים ופריסת הקבצים הבאים:

├── cmd
│   ├── go.mod
│   └── main.go
└── logging
    ├── go.mod
    └── logging.go

עכשיו שהשלמנו את כל התצורה, אנו יכולים להריץ את התכנית main מהחבילה cmd עם הפקודות הבאות:

  1. cd ../cmd
  2. go run main.go

תקבל פלט דומה למה שבא להלן:

Output
2019-08-28T11:36:09-05:00 This is a debug statement...

התכנית תדפיס את השעה הנוכחית בפורמט RFC 3339, ולאחר מכן את ההצהרה ששלחנו ליומן. RFC 3339 הוא פורמט זמן שתוכנן לייצוג זמן באינטרנט ומשמש בדרך כלל בקבצי יומן.

מכיוון שהפונקציות Debug ו-Log מיוצאות מהחבילה ליומן, אנו יכולים להשתמש בהן בחבילת main. עם זאת, המשתנה debug בחבילת logging אינו מיוצא. ניסיון להתייחס להצהרה לא מיוצאת יגרום לשגיאת קומפילציה.

הוסף את השורה המודגשת הבאה ל-main.go:

cmd/main.go
package main

import "github.com/gopherguides/logging"

func main() {
	logging.Debug(true)

	logging.Log("This is a debug statement...")

	fmt.Println(logging.debug)
}

שמור והרץ את הקובץ. תקבל שגיאה דומה למה שבא להלן:

Output
. . . ./main.go:10:14: cannot refer to unexported name logging.debug

עכשיו שראינו כיצד פריטים מיוצאים ו-לא מיוצאים בחבילות מתנהגים, נבחן בהמשך כיצד שדות ו-שיטות יכולים להיות מיוצאים מ-מבנים.

נראות בתוך מבנים

בעוד שתבנית הנראות בlogger שבנינו בסעיף הקודם עשויה לעבוד עבור תוכניות פשוטות, היא חולקת מדי הרבה מצב כדי להיות שימושית מתוך מספר חבילות. זה בגלל שהמשתנים המיוצאים נגישים למספר חבילות שעשויות לשנות את המשתנים למצבים סותרים. מתן האפשרות למצב של החבילה שלך להשתנות בדרך זו מקשה על צפייה באופן פעולת התוכנית שלך. עם העיצוב הנוכחי, לדוגמה, חבילה אחת יכולה להגדיר את המשתנה Debug ל-true, וחבילה אחרת יכולה להגדיר אותו ל-false באותה מופע. זה ייצור בעיה מאחר ששתי החבילות שמייבאות את החבילה logging מושפעות.

אנו יכולים לבודד את הlogger על ידי יצירת struct ואז לתלות שיטות עליו. זה יאפשר לנו ליצור מופע של logger לשימוש באופן עצמאי בכל חבילה שמשתמשת בו.

שנה את החבילה logging למצב הבא כדי לעבור על הקוד ולבודד את הlogger:

logging/logging.go
package logging

import (
	"fmt"
	"time"
)

type Logger struct {
	timeFormat string
	debug      bool
}

func New(timeFormat string, debug bool) *Logger {
	return &Logger{
		timeFormat: timeFormat,
		debug:      debug,
	}
}

func (l *Logger) Log(s string) {
	if !l.debug {
		return
	}
	fmt.Printf("%s %s\n", time.Now().Format(l.timeFormat), s)
}

בקוד זה, יצרנו מבנה Logger. מבנה זה יארח את המצב הלא מיובא שלנו, כולל פורמט הזמן להדפסה והמשתנה debug שמוגדר כ-true או false. הפונקציה New מגדירה את המצב הראשוני ליצירת הלוגר, כגון פורמט הזמן ומצב הדיבאג. לאחר מכן היא מאחסנת את הערכים שהעברנו לה בתוך המשתנים הלא מיובאים timeFormat ו-debug. יצרנו גם שיטה בשם Log על סוג Logger שמקבלת משפט שאנו רוצים להדפיס. בתוך השיטה Log יש הפניה למשתנה המקומי של השיטה l כדי לקבל גישה חזרה לשדות הפנימיים שלה כמו l.timeFormat ו-l.debug.

גישה זו תאפשר לנו ליצור Logger בחבילות רבות שונות ולהשתמש בו באופן עצמאי מכיצד החבילות האחרות משתמשות בו.

כדי להשתמש בו בחבילה אחרת, בואו נשנה את cmd/main.go להיראות כמו הבא:

cmd/main.go
package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, true)

	logger.Log("This is a debug statement...")
}

הרצת התוכנית תיתן לך את הפלט הבא:

Output
2019-08-28T11:56:49-05:00 This is a debug statement...

בקוד זה, יצרנו מופע של הלוגר על ידי קריאה לפונקציה המיובאת New. אחסנו את ההפניה למופע זה במשתנה logger. כעת ניתן לקרוא ל-logging.Log כדי להדפיס משפטים.

אם ננסה להפנות לשדה לא מיובא מה-Logger כמו השדה timeFormat, נקבל שגיאת קומפילציה. נסה להוסיף את השורה המודגשת הבאה ולהריץ את cmd/main.go:

cmd/main.go

package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, true)

	logger.Log("This is a debug statement...")

	fmt.Println(logger.timeFormat)
}

זה ייתן את השגיאה הבאה:

Output
. . . cmd/main.go:14:20: logger.timeFormat undefined (cannot refer to unexported field or method timeFormat)

המהדר מבחין ש-logger.timeFormat אינו מיוצא, ולכן לא ניתן לאחזר אותו מהחבילה logging.

נראות בתוך שיטות

באותו אופן כמו שדות מבנה, גם שיטות יכולות להיות מיוצאות או לא מיוצאות.

כדי להמחיש זאת, בואו נוסיף יומן דרגתי leveled ליומן שלנו. יומן דרגתי הוא דרך לסווג את היומנים שלך כך שתוכל לחפש ביומנים שלך עבור סוגים ספציפיים של אירועים. הרמות שנכניס ליומן שלנו הן:

  • רמת info, המייצגת אירועים מסוג מידע שמודיעים למשתמש על פעולה, כגון התוכנית התחילה, או הודעת דואר אלקטרוני נשלחה. אלו עוזרות לנו לנפות באגים ולעקוב אחר חלקים מהתוכנית שלנו כדי לראות אם ההתנהגות הצפויה מתרחשת.

  • רמת warning. אירועים אלו מזהים מתי משהו לא צפוי קורה שאינו שגיאה, כמו הודעת דואר אלקטרוני נכשלה לשלוח, ניסיון חוזר. הם עוזרים לנו לראות חלקים מהתוכנית שלנו שאינם מתנהלים בצורה חלקה כפי שציפינו.

  • הרמה `error`, מה שאומר שהתכנית נתקלה בבעיה, כמו `File not found`. זה לעתים קרובות יגרום לכישלון בפעולת התכנית.

ייתכן שתרצה גם להפעיל ולכבות רמות מסוימות של רישום, במיוחד אם התכנית שלך לא מבצעת כפי שצפוי ותרצה לנפות את התכנית. נוסיף פונקציונליות זו על ידי שינוי התכנית כך שכאשר `debug` מוגדר ל-`true`, הוא ידפיס את כל רמות הודעות. אחרת, אם זה `false`, הוא ידפיס רק הודעות שגיאה.

הוסף רישום מתואם על ידי ביצוע השינויים הבאים ל-`logging/logging.go`:

logging/logging.go

package logging

import (
	"fmt"
	"strings"
	"time"
)

type Logger struct {
	timeFormat string
	debug      bool
}

func New(timeFormat string, debug bool) *Logger {
	return &Logger{
		timeFormat: timeFormat,
		debug:      debug,
	}
}

func (l *Logger) Log(level string, s string) {
	level = strings.ToLower(level)
	switch level {
	case "info", "warning":
		if l.debug {
			l.write(level, s)
		}
	default:
		l.write(level, s)
	}
}

func (l *Logger) write(level string, s string) {
	fmt.Printf("[%s] %s %s\n", level, time.Now().Format(l.timeFormat), s)
}

בדוגמה זו, הצגנו טיעון חדש למתודה `Log`. כעת ניתן להעביר את ה-`level` של הודעת היומן. המתודה `Log` קובעת איזו רמת הודעה זה. אם זו הודעת `info` או `warning`, והשדה `debug` הוא `true`, אז היא כותבת את ההודעה. אחרת היא מתעלמת מההודעה. אם זו כל רמה אחרת, כמו `error`, היא תכתוב את ההודעה ללא קשר.

רוב ההגיון לקביעה אם הודעה תודפס קיים במתודה Log. הוספנו גם מתודה לא מיוצאת בשם write. המתודה write היא מה שבאמת מוציא את הודעת היומן.

נוכל כעת להשתמש ביומן מפורט זה בחבילה האחרת שלנו על ידי שינוי cmd/main.go להיראות כמו הבא:

cmd/main.go
package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, true)

	logger.Log("info", "starting up service")
	logger.Log("warning", "no tasks found")
	logger.Log("error", "exiting: no work performed")

}

הרצת זה תיתן לך:

Output
[info] 2019-09-23T20:53:38Z starting up service [warning] 2019-09-23T20:53:38Z no tasks found [error] 2019-09-23T20:53:38Z exiting: no work performed

בדוגמה זו, cmd/main.go השתמש בהצלחה במתודה Log המיוצאת.

נוכל כעת להעביר את level של כל הודעה על ידי החלפת debug ל-false:

main.go
package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, false)

	logger.Log("info", "starting up service")
	logger.Log("warning", "no tasks found")
	logger.Log("error", "exiting: no work performed")

}

עכשיו נראה שרק הודעות ברמת error מודפסות:

Output
[error] 2019-08-28T13:58:52-05:00 exiting: no work performed

אם ננסה לקרוא למתודה write מחוץ לחבילה logging, נקבל שגיאת קומפילציה:

main.go
package main

import (
	"time"

	"github.com/gopherguides/logging"
)

func main() {
	logger := logging.New(time.RFC3339, true)

	logger.Log("info", "starting up service")
	logger.Log("warning", "no tasks found")
	logger.Log("error", "exiting: no work performed")

	logger.write("error", "log this message...")
}
Output
cmd/main.go:16:8: logger.write undefined (cannot refer to unexported field or method logging.(*Logger).write)

כשהקומפיילר רואה שאתה מנסה להתייחס למשהו מחבילה אחרת שמתחיל באות קטנה, הוא יודע שזה לא מיוצא, ולכן זורק שגיאת קומפיילציה.

הלוגר במדריך זה ממחיש כיצד אנו יכולים לכתוב קוד שמגלה רק את החלקים שאנו רוצים שחבילות אחרות יצרכו. מכיוון שאנו שולטים בחלקים מהחבילה שגלויים מחוץ לחבילה, אנו יכולים כעת לבצע שינויים עתידיים מבלי להשפיע על כל קוד שתלוי בחבילה שלנו. לדוגמה, אם היינו רוצים לכבות רק הודעות ברמת info כאשר debug הוא שקר, תוכל לבצע שינוי זה מבלי להשפיע על כל חלק אחר של ה-API שלך. יכולנו גם לבצע שינויים בטוחים בהודעת הלוג כדי לכלול מידע נוסף, כמו התיקייה שממנה התוכנית רצה.

מסקנה

מאמר זה הראה כיצד לשתף קוד בין חבילות תוך גם מיגון פרטי היישום של החבילה שלך. זה מאפשר לך לייצא API פשוט שישתנה לעתים רחוקות מסיבות תאימות לאחור, אך יאפשר שינויים פרטיים בחבילה שלך כפי שנדרש כדי להפוך אותה לעבוד טוב יותר בעתיד. זה נחשב לטובה העיקרית בעת יצירת חבילות וה-APIs המקבילים להן.

כדי ללמוד עוד על חבילות ב-Go, עיין במאמרינו ייבוא חבילות ב-Go ו-כיצד לכתוב חבילות ב-Go, או חקור את כל הסדרה שלנו כיצד לתכנת ב-Go.

Source:
https://www.digitalocean.com/community/tutorials/understanding-package-visibility-in-go