One place for hosting & domains

      How To Set Up the Eclipse Theia Cloud IDE Platform on CentOS 7


      The author selected the Free and Open Source Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      With developer tools moving to the cloud, adoption of cloud IDE (Integrated Development Environment) platforms is growing. Cloud IDEs are accessible from every type of modern device through web browsers, and they offer numerous advantages for real-time collaboration scenarios. Working in a cloud IDE provides a unified development and testing environment for you and your team, while minimizing platform incompatibilities. Accessible through web browsers, cloud IDEs are available from every type of modern device.

      Eclipse Theia is an extensible cloud IDE running on a remote server and accessible from a web browser. Visually, it’s designed to look and behave similarly to Microsoft Visual Studio Code, which means that it supports many programming languages, has a flexible layout, and has an integrated terminal. What separates Eclipse Theia from other cloud IDE software is its extensibility; it can be modified using custom extensions, which allow you to craft a cloud IDE suited to your needs.

      In this tutorial, you’ll deploy Eclipse Theia to your CentOS 7 server using Docker Compose, a container orchestration tool. You’ll expose it at your domain using nginx-proxy, an automated system for Docker that simplifies the process of configuring Nginx to serve as a reverse proxy for a container. You’ll also secure it using a free Let’s Encrypt TLS certificate, which you’ll provision using its specialized add-on. In the end, you’ll have Eclipse Theia running on your CentOS 7 server available via HTTPS and requiring the user to log in.

      Prerequisites

      Step 1 — Deploying nginx-proxy with Let’s Encrypt

      In this section, you’ll deploy nginx-proxy and its Let’s Encrypt add-on using Docker Compose. This will enable automatic TLS certificate provisioning and renewal, so that when you deploy Eclipse Theia it will be accessible at your domain via HTTPS.

      For the purposes of this tutorial, you’ll store all files under ~/eclipse-theia. Create the directory by running the following command:

      Navigate to it:

      You’ll store the Docker Compose configuration for nginx-proxy in a file named nginx-proxy-compose.yaml. Create it using your text editor:

      • vi nginx-proxy-compose.yaml

      Add the following lines:

      ~/eclipse-theia/nginx-proxy-compose.yaml

      version: '2'
      
      services:
        nginx-proxy:
          restart: always
          image: jwilder/nginx-proxy
          ports:
            - "80:80"
            - "443:443"
          volumes:
            - "/etc/nginx/htpasswd:/etc/nginx/htpasswd"
            - "/etc/nginx/vhost.d"
            - "/usr/share/nginx/html"
            - "/var/run/docker.sock:/tmp/docker.sock:ro"
            - "/etc/nginx/certs"
      
        letsencrypt-nginx-proxy-companion:
          restart: always
          image: jrcs/letsencrypt-nginx-proxy-companion
          volumes:
            - "/var/run/docker.sock:/var/run/docker.sock:ro"
          volumes_from:
            - "nginx-proxy"
      

      Here you’re defining two services that Docker Compose will run, nginx-proxy and its Let’s Encrypt companion. For the proxy, you specify jwilder/nginx-proxy as the image, map HTTP and HTTPS ports, and define volumes that will be accessible to it during runtime.

      Volumes are directories on your server that the defined service will have full access to, which you’ll later use to set up user authentication. To achieve that, you’ll make use of the first volume from the list, which maps the local /etc/nginx/htpasswd directory to the same one in the container. In that folder, nginx-proxy expects to find a file named exactly as the target domain, containing log-in credentials for user authentication in the htpasswd format (username:hashed_password).

      For the add-on, you name the Docker image and allow access to Docker’s socket by defining a volume. Then, you specify that the add-on should inherit access to the volumes defined for nginx-proxy. Both services have restart set to always, which orders Docker to restart the containers in case of crash or system reboot.

      Save and close the file.

      Deploy the configuration by running:

      • docker-compose -f nginx-proxy-compose.yaml up -d

      Here you pass in the nginx-proxy-compose.yaml filename to the -f parameter of the docker-compose command, which specifies the file to run. Then, you pass the up verb that instructs it to run the containers. The -d flag enables detached mode, which means that Docker Compose will run the containers in the background.

      The final output will look like this:

      Output

      Creating network "eclipse-theia_default" with the default driver Pulling nginx-proxy (jwilder/nginx-proxy:)... latest: Pulling from jwilder/nginx-proxy 8d691f585fa8: Pull complete 5b07f4e08ad0: Pull complete ... Digest: sha256:dfc0666b9747a6fc851f5fb9b03e65e957b34c95d9635b4b5d1d6b01104bde28 Status: Downloaded newer image for jwilder/nginx-proxy:latest Pulling letsencrypt-nginx-proxy-companion (jrcs/letsencrypt-nginx-proxy-companion:)... latest: Pulling from jrcs/letsencrypt-nginx-proxy-companion 89d9c30c1d48: Pull complete 668840c175f8: Pull complete ... Digest: sha256:a8d369d84079a923fdec8ce2f85827917a15022b0dae9be73e6a0db03be95b5a Status: Downloaded newer image for jrcs/letsencrypt-nginx-proxy-companion:latest Creating eclipse-theia_nginx-proxy_1 ... done Creating eclipse-theia_letsencrypt-nginx-proxy-companion_1 ... done

      You’ve deployed nginx-proxy and its Let’s Encrypt companion using Docker Compose. Now you’ll move on to set up Eclipse Theia at your domain and secure it.

      Step 2 — Deploying Dockerized Eclipse Theia

      In this section, you’ll create a file containing any allowed log-in combinations that a user will need to input. Then, you’ll deploy Eclipse Theia to your server using Docker Compose and expose it at your secured domain using nginx-proxy.

      As explained in the previous step, nginx-proxy expects log-in combinations to be in a file named after the exposed domain, in the htpasswd format and stored under the /etc/nginx/htpasswd directory in the container. The local directory which maps to the virtual one does not need to be the same, as was specified in the nginx-proxy config.

      To create log-in combinations, you’ll first need to install htpasswd by running the following command:

      • sudo yum install httpd-tools

      The httpd-tools package contains the htpasswd utility.

      Create the /etc/nginx/htpasswd directory:

      • sudo mkdir -p /etc/nginx/htpasswd

      Create a file that will store the logins for your domain:

      • sudo touch /etc/nginx/htpasswd/theia.your-domain

      Remember to replace theia.your-domain with your Eclipse Theia domain.

      To add a username and password combination, run the following command:

      • sudo htpasswd /etc/nginx/htpasswd/theia.your-domain username

      Replace username with the username you want to add. You’ll be asked for a password twice. After providing it, htpasswd will add the username and hashed password pair to the end of the file. You can repeat this command for as many logins as you wish to add.

      Now, you’ll create configuration for deploying Eclipse Theia. You’ll store it in a file named eclipse-theia-compose.yaml. Create it using your text editor:

      • vi eclipse-theia-compose.yaml

      Add the following lines:

      ~/eclipse-theia/eclipse-theia-compose.yaml

      version: '2.2'
      
      services:
        eclipse-theia:
          restart: always
          image: theiaide/theia:next
          init: true
          environment:
            - VIRTUAL_HOST=theia.your-domain
            - LETSENCRYPT_HOST=theia.your-domain
      

      In this config, you define a single service called eclipse-theia with restart set to always and theiaide/theia:next as the container image. You also set init to true to instruct Docker to use init as the main process manager when running Eclipse Theia inside the container.

      Then, you specify two environment variables in the environment section: VIRTUAL_HOST and LETSENCRYPT_HOST. The former is passed on to nginx-proxy and tells it at what domain the container should be exposed, while the latter is used by its Let’s Encrypt add-on and specifies for which domain to request TLS certificates. Unless you specify a wildcard as the value for VIRTUAL_HOST, they must be the same.

      Remember to replace theia.your-domain with your desired domain, then save and close the file.

      Now deploy Eclipse Theia by running:

      • docker-compose -f eclipse-theia-compose.yaml up -d

      The final output will look like:

      Output

      ... Pulling eclipse-theia (theiaide/theia:next)... next: Pulling from theiaide/theia 63bc94deeb28: Pull complete 100db3e2539d: Pull complete ... Digest: sha256:c36dff04e250f1ac52d13f6d6e15ab3e9b8cad9ad68aba0208312e0788ecb109 Status: Downloaded newer image for theiaide/theia:next Creating eclipse-theia_eclipse-theia_1 ... done

      Then, in your browser, navigate to the domain you’re using for Eclipse Theia. Your browser will show you a prompt asking you to log in. After providing the correct credentials, you’ll enter Eclipse Theia and immediately see its editor GUI. In your address bar you’ll see a padlock indicating that the connection is secure. If you don’t see this immediately, wait a few minutes for the TLS certificates to provision, then reload the page.

      Eclipse Theia GUI

      Now that you can securely access your cloud IDE, you’ll start using the editor in the next step.

      Step 3 — Using the Eclipse Theia Interface

      In this section, you’ll explore some of the features of the Eclipse Theia interface.

      On the left-hand side of the IDE, there is a vertical row of four buttons opening the most commonly used features in a side panel.

      Eclipse Theia GUI - Sidepanel

      This bar is customizable so you can move these views to a different order or remove them from the bar. By default, the first view opens the Explorer panel that provides tree-like navigation of the project’s structure. You can manage your folders and files here—creating, deleting, moving, and renaming them as necessary.

      After creating a new file through the File menu, you’ll see an empty file open in a new tab. Once saved, you can view the file’s name in the Explorer side panel. To create folders right click on the Explorer sidebar and click on New Folder. You can expand a folder by clicking on its name as well as dragging and dropping files and folders to upper parts of the hierarchy to move them to a new location.

      Eclipse Theia GUI - New Folder

      The next two options provide access to search and replace functionality. Following it, the next one provides a view of source control systems that you may be using, such as Git.

      The final view is the debugger option, which provides all the common actions for debugging in the panel. You can save debugging configurations in the launch.json file.

      Debugger View with launch.json open

      The central part of the GUI is your editor, which you can separate by tabs for your code editing. You can change your editing view to a grid system or to side-by-side files. Like all modern IDEs, Eclipse Theia supports syntax highlighting for your code.

      Editor Grid View

      You can gain access to a terminal by typing CTRL+SHIFT+`, or by clicking on Terminal in the upper menu, and selecting New Terminal. The terminal will open in a lower panel and its working directory will be set to the project’s workspace, which contains the files and folders shown in the Explorer side panel.

      Terminal open

      You’ve explored a high-level overview of the Eclipse Theia interface and reviewed some of the most commonly used features.

      Conclusion

      You now have Eclipse Theia, a versatile cloud IDE, installed on your CentOS 7 server using Docker Compose and nginx-proxy. You’ve secured it with a free Let’s Encrypt TLS certificate and set up the instance to require log-in credentials from the user. You can work on your source code and documents with it individually or collaborate with your team. You can also try building your own version of Eclipse Theia if you need additional functionality. For further information on how to do that, visit the Theia docs.



      Source link

      Знакомство с картами в Go


      В большинстве современных языков программирования применяется концепция словаря или хэша. Эти типы обычно используются для хранения данных в парах, где ключ соответствует значению.

      В Go тип данных map используется как тип dictionary в большинстве других языков программирования. Он сопоставляет ключи со значениями, создавая пары ключ-значение, представляющие собой полезный способ хранения данных в Go. Построение карты осуществляется с помощью ключевого слова map с последующим указанием типа данных ключа в квадратных скобках [ ] и типа данных значения. Пары ключ-значение заключаются в фигурные скобки { }:

      map[key]value{}
      

      Карты в Go обычно используются для хранения связанных данных, например, содержащейся в идентификаторах информации. Карта с данными выглядит следующим образом:

      map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
      

      В дополнение к фигурным скобкам в карте используются двоеточия, соединяющие пары ключ-значение. Слова слева от двоеточий являются ключами. Ключи могут иметь любой сравниваемый тип в Go, в том числе строки, целые числа и т. д.

      Ключи в примере карты:

      • "name"
      • "animal"
      • "color"
      • "location"

      Слова справа от двоеточий являются значениями. Значения могут относиться к любому типу данных. Значения в примере карты:

      • "Sammy"
      • "shark"
      • "blue"
      • "ocean"

      Как и другие типы данных, карты могут храниться в переменных и выводиться:

      sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
      fmt.Println(sammy)
      

      Результат будет выглядеть так:

      Output
      map[animal:shark color:blue location:ocean name:Sammy]

      Порядок пар ключ-значение можно изменять. Тип данных карт Go является неупорядоченным. Вне зависимости от порядка, пары ключ-значение останутся без изменений, что даст возможность доступа к данным по их относительному значению.

      Доступ к элементам карты

      Вы можете вызывать значения карты, ссылаясь на соответствующие ключи. Поскольку карты предоставляют пары ключ-значение для хранения данных, они могут стать полезным элементом вашей программы Go.

      Если вы хотите изолировать имя пользователя Sammy, вы можете вызвать для этого переменную sammy["name"], которая содержит карту и связанный ключ. Распечатаем результат:

      fmt.Println(sammy["name"])
      

      Получим значение в качестве результата:

      Output
      Sammy

      Карты похожи на базы данных, однако вместо вызова целого числа для получения определенного значения индекса, как в срезе, вы присваиваете значение ключу и вызываете ключ для получения связанного значения.

      Вызывая ключ "name", вы получаете значение этого ключа, то есть "Sammy".

      Также вы можете вызывать остальные значения карты sammy в том же формате:

      fmt.Println(sammy["animal"])
      // returns shark
      
      fmt.Println(sammy["color"])
      // returns blue
      
      fmt.Println(sammy["location"])
      // returns ocean
      

      Используя пары ключ-значение в типах данных карт, вы можете ссылаться на ключи для получения значений.

      Ключи и значения

      В отличие от некоторых языков программирования, в Go отсутствуют удобные функции вывода списка ключей или значений карты. В качестве примера такой функции можно назвать метод Python .keys() для словарей. Однако он поддерживает итерацию с использованием оператора range:

      for key, value := range sammy {
          fmt.Printf("%q is the key for the value %qn", key, value)
      }
      

      При ранжировании карты в Go выводится два значения. Первое значение будет ключом, а второе — значением. Go создаст эти переменные с правильным типом данных. В данном случае ключ карты представлял собой строку, поэтому ключ также будет строкой. Значение также является строкой:

      Output
      "animal" is the key for the value "shark" "color" is the key for the value "blue" "location" is the key for the value "ocean" "name" is the key for the value "Sammy"

      Чтобы получить только список ключей, вы можете еще раз использовать оператор range. Чтобы получить доступ только к ключам, вы можете декларировать только одну переменную:

      keys := []string{}
      
      for key := range sammy {
          keys = append(keys, key)
      }
      fmt.Printf("%q", keys)
      

      Вначале программа декларирует срез для хранения ваших ключей.

      В результатах будут показаны только ключи вашей карты:

      Output
      ["color" "location" "name" "animal"]

      Ключи не сортируются. Если вы хотите сортировать их, используйте функцию sort.Strings из пакета sort:

      sort.Strings(keys)
      

      С помощью этой функции вы получите следующие результаты:

      Output
      ["animal" "color" "location" "name"]

      Вы можете использовать тот же шаблон для получения значений карты. В следующем примере вы предварительно выделите заранее срез для предотвращения подобных действий, что сделает программу более эффективной:

      sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
      
      items := make([]string, len(sammy))
      
      var i int
      
      for _, v := range sammy {
          items[i] = v
          i++
      }
      fmt.Printf("%q", items)
      

      Вначале вы декларируете срез для хранения ключей. Поскольку вы знаете, сколько вам требуется элементов, вы можете избежать несоответствия при выделении памяти, определив точный размер среза. После этого вы декларируете переменную индекса. Поскольку вам не нужен ключ, вы используете оператор _ при запуске цикла, чтобы игнорировать значение ключа. Результат будет выглядеть так:

      Output
      ["ocean" "Sammy" "shark" "blue"]

      Чтобы определить количество элементов на карте, вы можете использовать встроенную функцию len:

      sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
      fmt.Println(len(sammy))
      

      В результатах будет показано количество элементов на вашей карте:

      Output
      4

      Хотя в Go отсутствуют удобные функции получения ключей и значений, при необходимости их можно получить с помощью нескольких строчек кода.

      Проверка существования

      Если запрошенный ключ отсутствует, карты Go возвращают нулевое значение для типа значений карты. Поэтому вам нужен альтернативный способ отличить сохраненный ноль от отсутствующего ключа.

      Поищем в карте заведомо несуществующее значение и посмотрим на результат:

      counts := map[string]int{}
      fmt.Println(counts["sammy"])
      

      Вывод должен выглядеть так:

      Output
      0

      Хотя ключ sammy отсутствует в карте, Go возвращает значение 0. Это связано с тем, что используется тип данных значения int, и поскольку в Go задано нулевое значение всех переменных, возвращается нулевое значение 0.

      Во многих случаях такое поведение нежелательно и может вызвать ошибку в программе. При поиске значения на карте Go может вывести второе опциональное значение. Это второе значение является булевым значением true в случае обнаружения ключа или false в случае отсутствия ключа. В Go это называется идиомой ok. Хотя вы можете присвоить любое имя переменной, получающей второй аргумент, в Go всегда следует использовать имя ok:

      count, ok := counts["sammy"]
      

      Если ключ sammy существует в карте counts, ok будет иметь значение true. В противном случае ok будет иметь значение false.

      Вы можете использовать переменную ok для определения действий в программе:

      if ok {
          fmt.Printf("Sammy has a count of %dn", count)
      } else {
          fmt.Println("Sammy was not found")
      }
      

      Результат будет выглядеть следующим образом:

      Output
      Sammy was not found

      В Go вы можете комбинировать декларирование переменных и условную проверку с помощью блока if/else. Это позволяет использовать для такой проверки одно выражение:

      if count, ok := counts["sammy"]; ok {
          fmt.Printf("Sammy has a count of %dn", count)
      } else {
          fmt.Println("Sammy was not found")
      }
      

      При получении значения из карты в Go всегда полезно проверить его существование, чтобы избежать ошибок в программе.

      Изменение карт

      Карты — это мутируемая структура данных, и вы можете изменять их. В этом разделе мы рассмотрим добавление и удаление элементов карт.

      Добавление и изменение элементов карт

      Вы можете добавлять на карты пары ключ-значение, не используя методы или функции. Для этого используется имя переменной карт, за которым идет значение ключа в квадратных скобках [ ] и оператор равенства = для определения нового значения:

      map[key] = value
      

      Покажем это на примере добавления пары ключ-значение в карту с именем usernames:

      usernames := map[string]string{"Sammy": "sammy-shark", "Jamie": "mantisshrimp54"}
      
      usernames["Drew"] = "squidly"
      fmt.Println(usernames)
      

      В результате на карте будет выведена новая пара ключ-значение Drew:squidly:

      Output
      map[Drew:squidly Jamie:mantisshrimp54 Sammy:sammy-shark]

      Поскольку карты выводятся в неупорядоченном виде, эта пара может появиться в карте где угодно. Если вы используете карту usernames в следующих частях вашего файла программы, она будет содержать дополнительную пару ключ-значение.

      Также этот синтаксис можно использовать для изменения значения, назначенного ключу. В этом случае вы ссылаетесь на существующий ключ и передаете ему новое значение.

      Рассмотрим карту с именем followers, которая отслеживает подписчиков пользователей определенной сети. У пользователя "drew" сегодня выросло число подписчиков, и поэтому нужно обновить целочисленное значение, которое передается для ключа "drew". Мы используем функцию Println() для проверки изменения карты:

      followers := map[string]int{"drew": 305, "mary": 428, "cindy": 918}
      followers["drew"] = 342
      fmt.Println(followers)
      

      В результатах будет показано обновленное значение drew:

      Output
      map[cindy:918 drew:342 mary:428]

      Мы видим, что количество подписчиков увеличилось с целого числа 305 до 342.

      Вы можете использовать этот метод для добавления пар ключ-значение в карты с вводимыми пользователем данными. Напишем небольшую программу usernames.go, которая выполняется в командной строке и позволяет пользователю вводить дополнительные имена и связанные с ними имена пользователей:

      usernames.go
      package main
      
      import (
          "fmt"
          "strings"
      )
      
      func main() {
          usernames := map[string]string{"Sammy": "sammy-shark", "Jamie": "mantisshrimp54"}
      
          for {
              fmt.Println("Enter a name:")
      
              var name string
              _, err := fmt.Scanln(&name)
      
              if err != nil {
                  panic(err)
              }
      
              name = strings.TrimSpace(name)
      
              if u, ok := usernames[name]; ok {
                  fmt.Printf("%q is the username of %qn", u, name)
                  continue
              }
      
              fmt.Printf("I don't have %v's username, what is it?n", name)
      
              var username string
              _, err = fmt.Scanln(&username)
      
              if err != nil {
                  panic(err)
              }
      
              username = strings.TrimSpace(username)
      
              usernames[name] = username
      
              fmt.Println("Data updated.")
          }
      }
      

      Вначале мы определим в файле usernames.go первоначальную карту. Затем мы зададим цикл итерации имен. Мы предлагаем пользователю ввести имя и декларируем переменную для его сохранения. Затем мы проверяем наличие ошибок, и если они есть, в программе возникает паника, и она закрывается. Поскольку Scanln получает все вводимые данные, включая символ возврата каретки, нам нужно удалить из вводимых данных все пробелы. Для этого мы используем функцию strings.TrimSpace.

      Блок if проверяет наличие имени на карте и выводит обратную связь. Если имя присутствует на карте, программа возвращается в начало цикла. Если имени нет на карте, пользователю направляется обратная связь и предлагается ввести новое имя пользователя для данного имени. Затем программа снова проверяет наличие ошибок. При отсутствии ошибок программа удаляет символ возврата каретки, назначает значение имени пользователя ключу name и выводит сообщение об обновлении данных.

      Запустим эту программу в командной строке:

      • go run usernames.go

      Вывод должен выглядеть так:

      Output
      Enter a name: Sammy "sammy-shark" is the username of "Sammy" Enter a name: Jesse I don't have Jesse's username, what is it? JOctopus Data updated. Enter a name:

      После завершения тестирования нажмите CTRL + C для выхода из программы.

      Это показывает возможность интерактивного изменения карт. В данной конкретной программе при выходе с помощью клавиш CTRL + C вы потеряете все данные, если не реализуете способ чтения и записи файлов.

      Таким образом, мы можем добавлять элементы на карты или изменять их значения с помощью синтаксиса map[key] = value.

      Удаление элементов карт

      Мы можем не только добавлять пары ключ-значение и изменять значения типа данных карты, но и удалять элементы карт.

      Чтобы удалить пару ключ-значение с карты, можно использовать встроенную функцию delete(). Первый аргумент — это карта, откуда мы удаляем элемент. Второй аргумент — это удаляемый ключ:

      delete(map, key)
      

      Определим карту разрешений:

      permissions := map[int]string{1: "read", 2: "write", 4: "delete", 8: "create", 16:"modify"}
      

      Нам больше не нужно разрешение modify, поэтому мы удалим его с нашей карты. Затем мы выведем карту, чтобы убедиться в его удалении:

      permissions := map[int]string{1: "read", 2: "write", 4: "delete", 8: "create", 16: "modify"}
      delete(permissions, 16)
      fmt.Println(permissions)
      

      Результат подтвердит удаление:

      Output
      map[1:read 2:write 4:delete 8:create]

      Строка delete(permissions, 16) удаляет пару ключ-значение 16:"modify" из карты permissions.

      Если вы хотите очистить карту от всех значений, вы можете задать ее равной пустой карте того же типа. При этом будет создана новая пустая карта, а сборщик мусора очистит память от старой карты.

      Удалим все элементы из карты permissions:

      permissions = map[int]string{}
      fmt.Println(permissions)
      

      Результаты показывают, что мы получили пустую карту без пар ключ-значение:

      Output
      map[]

      Поскольку карты имеют мутируемый тип данных, они поддерживают добавление, изменение, удаление и очистку элементов.

      Заключение

      В этом материале мы рассказали о структуре карт в Go. Карты состоят из пар ключ-значение и предоставляют возможность хранения данных без использования индексов. Это позволяет получать значения на основе их смысла и связи с другими типами данных.



      Source link

      Видимость пакетов в Go


      Введение

      При создании пакета в 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, учитывая при этом, что мы хотим сделать видимым вне пакета, а что хотим оставить невидимым. Этот пакет logging будет отвечать за регистрацию любых сообщений нашей программы на консоли. Также он будет проверять уровень регистрации. Уровень описывает тип журнала регистрации и будет иметь одно из трех состояний: info, warning или error.

      Создайте в каталоге src каталог с именем logging, где будут размещены наши файлы регистрации:

      Перейдите в этот каталог:

      Используйте 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 %sn", time.Now().Format(time.RFC3339), statement)
      }
      

      В первой строчке этого кода декларируется пакет с именем logging. В этом пакете содержится две экспортируемые функции: Debug и Log. Эти функции сможет вызывать любой другой пакет, импортирующий пакет logging. Также существует private переменная с именем debug. Эта переменная доступна только из пакета logging. Важно отметить, что функция Debug и переменная debug имеют одинаковое написание, но имя функции начинается с заглавной буквы, а имя переменной — нет. Это обеспечивает отдельное декларирование с разной сферой действия.

      Сохраните и закройте файл.

      Чтобы использовать этот пакет в других частях нашего кода, мы можем импортировать его в новый пакет. Мы создадим этот новый пакет, но вначале нам потребуется новый каталог для хранения этих исходных файлов.

      Выйдем из каталога logging, создадим новый каталог cmd и перейдем в этот новый каталог:

      Создайте файл с именем main.go в каталоге cmd, который мы только что создали:

      Теперь мы можем добавить следующий код:

      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:

      Затем поместите в файл следующий код:

      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:

      • cd ../logging
      • 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 с помощью следующих команд:

      Результат должен выглядеть примерно следующим образом:

      Output

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

      Программа выведет текущее время в формате RFC 3339, а затем выражение, которое мы отправили в регистратор. RFC 3339 — это формат времени, разработанный для представления времени в интернете и обычно используемый в файлах журналов.

      Поскольку функции Debug и Log экспортированы из пакета logging, мы можем использовать их в нашем пакете 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

      Мы увидели поведение экспортированных и неэкспортированных элементов в пакетах, а теперь посмотрим, как можно экспортировать поля и методы из структур.

      Видимость в структурах

      Хотя построенная нами в предыдущем разделе схема видимости может работать для простых программ, она передает слишком много значений состояния, чтобы быть полезной в нескольких пакетах. Это связано с тем, что экспортированные переменные доступны многим пакетам, которые могут изменять переменные до конфликтующих состояний. Если разрешить подобное изменение состояния пакета, будет сложно прогнозировать поведение программы. Например, при текущей схеме один пакет может задать для переменной Debug значение true, а другой — значение false для того же самого экземпляра. Это создаст проблему, поскольку будет влиять на оба пакета, импортирующих пакет logging.

      Мы можем изолировать регистратор, создав структуру и передав ей методы. Это позволит нам создавать экземпляр регистратора, который будет использоваться независимо в каждом пакете.

      Замените пакет logging на следующее, чтобы исправить код и изолировать регистратор:

      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 %sn", time.Now().Format(l.timeFormat), s)
      }
      

      В этом коде мы создали структуру Logger. В этой структуре будет размещено неэкспортированное состояние, включая формат времени для вывода и значение переменной debugtrue или 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.

      Видимость в методах

      Методы, как и поля структуры, могут быть экспортируемыми или неэкспортируемыми.

      Для иллюстрации добавим в наш регистратор многоуровневую регистрацию. Многоуровневая регистрация — это способ разделения журналов регистрации на категории, позволяющий искать в журналах определенные типы событий. В нашем регистраторе мы используем следующие уровни:

      • Уровень info, представляющий события информационного типа, сообщающие пользователю о действии, например, Program started или Email sent. Они помогают выполнять отладку и отслеживать части программы, чтобы определять ожидаемое поведение.

      • Уровень warning. Эти события определяют непредвиденные события, которые не представляют собой ошибку, например, Email failed to send, retrying. Они помогают понять, какие части программы работают не так хорошо, как мы ожидали.

      • Уровень 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 %sn", level, time.Now().Format(l.timeFormat), s)
      }
      

      В этом примере мы ввели новый аргумент для метода Log. Теперь мы можем передать уровень сообщения журнала. Метод 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.

      Мы можем передать уровень каждого сообщения, изменив значение 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 имеет значение false, мы могли бы провести это изменение без воздействия на любую другую часть вашего API. Мы также могли безопасно вносить изменения в сообщения журнала, добавляя дополнительную информацию, такую как каталог, откуда запускается программа.

      Заключение

      В этой статье мы рассказали, как использовать в пакетах общий код и при этом защитить детали реализации конкретных пакетов. Это позволяет экспортировать простые API, которые редко изменяются для обеспечения обратной совместимости, но позволяют вносить в пакет необходимые изменения для улучшения их работы. Это считается лучшей практикой при создании пакетов и их API.

      Дополнительную информацию о пакетах в Go можно найти в статьях Импорт пакетов в Go и Написание пакетов в Go, а также в других статьях серии по программированию на Go.



      Source link